Changeset 96c714f in Klonkt for src/services/PushService.js


Ignore:
Timestamp:
07/22/2026 07:12:11 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
9a00f28
Parents:
8240e80
git-author:
Robin <roboburr@…> (07/22/2026 07:11:54 AM)
git-committer:
Robin <roboburr@…> (07/22/2026 07:12:11 AM)
Message:

Feature: web push slice 4, burst throttle + docs

  • Burst throttle: a wave of likes or a mass-follow becomes one ping, not a wave of pushes. Per (user, type) at most one push per window (like/boost 300s, follow 60s, reply/dm 30s, test never throttled); extras drop silently — the events themselves still land in Berichten, only the ping is deduped. In-memory (one process; a restart costs at most one extra ping). Pure throttled() exported and pinned by test.
  • README: push notifications feature bullet, VAPID_* in the config table, storage/.vapid in the auto-generated-secrets + backup section (restoring without it silently breaks every subscription).
  • .env.example: VAPID block in the SESSION_SECRET/PAID_SECRET style.

Pruning (404/410 → row deleted) and the iOS install hint already landed in
slices 1-2; this closes the plan from docs/webpush-design.md.

Changed files:
src/services/PushService.js

  • throttled() + window table; notifyUser checks it first

test/push.test.js

  • throttle windows, per-type/per-user independence, test bypass

README.md

  • feature bullet, VAPID config row, backup warning

.env.example

  • VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/PushService.js

    r8240e80 r96c714f  
    118118}
    119119
     120// Burst throttle: a wave of likes or a mass-follow must not become a wave of
     121// pushes. Per (user, type) at most one push per window; extras drop silently
     122// (the events themselves are still in Berichten — only the ping is deduped).
     123// In-memory is fine: one process, and a restart just means one extra ping.
     124const THROTTLE_SECONDS = { follow: 60, reply: 30, dm: 30, like: 300, boost: 300, test: 0 };
     125const _lastPush = new Map();
     126export function throttled(userId, type, nowSeconds = Math.floor(Date.now() / 1000)) {
     127  const windowS = THROTTLE_SECONDS[type] ?? 60;
     128  if (!windowS) return false;
     129  const key = `${userId}:${type}`;
     130  const prev = _lastPush.get(key) || 0;
     131  if (nowSeconds - prev < windowS) return true;
     132  _lastPush.set(key, nowSeconds);
     133  return false;
     134}
     135
    120136// Notify one user on all their devices, honouring per-type preferences.
    121137// type ∈ {follow, reply, like, boost, dm, test}. Fire-and-forget at call sites.
    122138export async function notifyUser(userId, { type, title, body, url }) {
    123139  if (!(await pushReady())) return 0;
     140  if (throttled(userId, type)) return 0;
    124141  const rows = db.prepare('SELECT * FROM push_subscriptions WHERE user_id = ?').all(userId);
    125142  let sent = 0;
     
    143160
    144161export default {
    145   publicKey, pushReady, DEFAULT_ALERTS,
     162  publicKey, pushReady, DEFAULT_ALERTS, throttled,
    146163  saveSubscription, deleteSubscription, listSubscriptions, updateAlerts,
    147164  notifyUser, notifySite,
Note: See TracChangeset for help on using the changeset viewer.