style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -33,11 +33,11 @@
|
||||
* T-05-19 — per-subscription try/catch; dispatchPush already swallows 410/404.
|
||||
*/
|
||||
|
||||
import { and, eq, gt, lte, sql } from 'drizzle-orm'
|
||||
import { db } from '../db/client.js'
|
||||
import { calendars, calendarEvents, pushSubscriptions } from '../db/schema.js'
|
||||
import { dispatchPush } from '../lib/pushDispatcher.js'
|
||||
import type { NotificationPayload } from '../lib/pushDispatcher.js'
|
||||
import { and, eq, gt, lte, sql } from 'drizzle-orm';
|
||||
import { db } from '../db/client.js';
|
||||
import { calendars, calendarEvents, pushSubscriptions } from '../db/schema.js';
|
||||
import { dispatchPush } from '../lib/pushDispatcher.js';
|
||||
import type { NotificationPayload } from '../lib/pushDispatcher.js';
|
||||
|
||||
// ── In-memory dedup (D-12: single-process, no Redis) ─────────────────────────
|
||||
// Key: event uid (bare string — no minuteBucket suffix).
|
||||
@@ -45,7 +45,7 @@ import type { NotificationPayload } from '../lib/pushDispatcher.js'
|
||||
// Prevents double-fire when the same event sits in the catch-up window across
|
||||
// multiple consecutive ticks (cross-tick exactly-once guarantee).
|
||||
// Acceptable data loss on process restart for a two-person household.
|
||||
const sentReminders = new Map<string, number>()
|
||||
const sentReminders = new Map<string, number>();
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -53,10 +53,10 @@ const sentReminders = new Map<string, number>()
|
||||
* Format a UTC Date as "YYYY-MM-DD" for calendar deep-link URLs.
|
||||
*/
|
||||
function yyyyMmDd(d: Date): string {
|
||||
const y = d.getUTCFullYear()
|
||||
const m = String(d.getUTCMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getUTCDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
const y = d.getUTCFullYear();
|
||||
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getUTCDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
// ── Core scan ─────────────────────────────────────────────────────────────────
|
||||
@@ -69,7 +69,7 @@ function yyyyMmDd(d: Date): string {
|
||||
* default = new Date() (current wall-clock time).
|
||||
*/
|
||||
export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
const windowEnd = new Date(now.getTime() + 16 * 60 * 1000)
|
||||
const windowEnd = new Date(now.getTime() + 16 * 60 * 1000);
|
||||
|
||||
// Single query: events (isShared, timed, in catch-up window) cross-joined with ALL
|
||||
// push_subscriptions. The cross-join (sql`1=1`) fans every matching event out
|
||||
@@ -99,21 +99,21 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
.innerJoin(pushSubscriptions, sql`1=1`)
|
||||
.where(
|
||||
and(
|
||||
eq(calendars.isShared, true), // D-05: shared calendar only (in QUERY, not copy)
|
||||
eq(calendarEvents.allDay, false), // D-07: timed events only
|
||||
gt(calendarEvents.dtstartUtc, now), // strictly future (excludes already-started)
|
||||
eq(calendars.isShared, true), // D-05: shared calendar only (in QUERY, not copy)
|
||||
eq(calendarEvents.allDay, false), // D-07: timed events only
|
||||
gt(calendarEvents.dtstartUtc, now), // strictly future (excludes already-started)
|
||||
lte(calendarEvents.dtstartUtc, windowEnd),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Group flat (event, subscription) rows by event uid so we can:
|
||||
// a) dedup per event (not per (event, sub) pair), and
|
||||
// b) fan out to ALL subscriptions for a deduped event in one pass.
|
||||
type SubRow = { id: number; userId: number; endpoint: string; p256dh: string; auth: string }
|
||||
type SubRow = { id: number; userId: number; endpoint: string; p256dh: string; auth: string };
|
||||
const byUid = new Map<
|
||||
string,
|
||||
{ uid: string; title: string | null; dtstartUtc: Date; subs: SubRow[] }
|
||||
>()
|
||||
>();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!byUid.has(row.uid)) {
|
||||
@@ -123,7 +123,7 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
// dtstartUtc is guaranteed non-null by the allDay=false + gt/lte WHERE
|
||||
dtstartUtc: row.dtstartUtc as Date,
|
||||
subs: [],
|
||||
})
|
||||
});
|
||||
}
|
||||
// subId is undefined when cross-join produces no subscriptions row (empty table)
|
||||
if (row.subId != null) {
|
||||
@@ -133,7 +133,7 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
endpoint: row.subEndpoint,
|
||||
p256dh: row.subP256dh,
|
||||
auth: row.subAuth,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,12 +143,12 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
// Per-uid exactly-once dedup (D-12): keyed on bare uid, no minuteBucket.
|
||||
// Same event fires at most once regardless of how many ticks it sits in window.
|
||||
// CAVEAT: rescheduling dtstart earlier after a reminder fired will not re-fire (v1).
|
||||
if (sentReminders.has(uid)) continue
|
||||
if (sentReminders.has(uid)) continue;
|
||||
|
||||
const dateStr = yyyyMmDd(event.dtstartUtc)
|
||||
const dateStr = yyyyMmDd(event.dtstartUtc);
|
||||
|
||||
// Lead-accurate body: compute actual minutes to start, guarded to minimum 1.
|
||||
const minutes = Math.max(1, Math.round((event.dtstartUtc.getTime() - now.getTime()) / 60000))
|
||||
const minutes = Math.max(1, Math.round((event.dtstartUtc.getTime() - now.getTime()) / 60000));
|
||||
|
||||
const notification: NotificationPayload = {
|
||||
// Null-safe title fallback (D-02 / NOTIF-01): title column may be NULL on rows
|
||||
@@ -157,12 +157,12 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
body: `Starts in ${minutes} min`,
|
||||
tag: `reminder-${uid}`,
|
||||
navigate: `/calendar?date=${dateStr}&event=${uid}`,
|
||||
}
|
||||
};
|
||||
|
||||
// Fan out to every subscriber (member-count-agnostic)
|
||||
for (const sub of event.subs) {
|
||||
try {
|
||||
await dispatchPush(sub, notification)
|
||||
await dispatchPush(sub, notification);
|
||||
} catch (err) {
|
||||
// Per-subscription error isolation (T-05-19): one bad sub never aborts the cycle.
|
||||
// dispatchPush itself never throws (it resolves after logging), so this outer
|
||||
@@ -170,20 +170,20 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
console.error(
|
||||
`[broker/reminderScheduler] Error dispatching reminder to sub id=${sub.id} for event uid=${uid}:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// WR-01: mark sent AFTER all dispatches have been attempted. Pre-marking before
|
||||
// dispatch prevents retry when dispatchPush throws — at-least-once delivery
|
||||
// requires not pre-marking the uid.
|
||||
sentReminders.set(uid, event.dtstartUtc.getTime())
|
||||
sentReminders.set(uid, event.dtstartUtc.getTime());
|
||||
} catch (err) {
|
||||
// Per-event error isolation (T-05-18): one bad event never aborts remaining events.
|
||||
console.error(
|
||||
`[broker/reminderScheduler] Error processing event uid=${uid}:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
// it can be removed safely (it will never re-enter the (now, now+16min] window).
|
||||
for (const [uid, dtstartMs] of sentReminders) {
|
||||
if (dtstartMs <= now.getTime()) {
|
||||
sentReminders.delete(uid)
|
||||
sentReminders.delete(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@ export async function runReminderCheck(now = new Date()): Promise<void> {
|
||||
export function startReminderScheduler(): void {
|
||||
setInterval(() => {
|
||||
runReminderCheck().catch((err: unknown) => {
|
||||
console.error('[broker/reminderScheduler] Unhandled runReminderCheck error:', err)
|
||||
})
|
||||
}, 60 * 1000)
|
||||
console.error('[broker/reminderScheduler] Unhandled runReminderCheck error:', err);
|
||||
});
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user