owltide/server/web-push.ts
Hornwitser 80cec71308
All checks were successful
/ build (push) Successful in 1m37s
/ deploy (push) Has been skipped
Use sync access for the temp JSON file database
Replace all async reads and writes to the JSON database with the sync
reads and writes to prevent a data corruption race condition where two
requests are processed at the same time and write to the same file, or
one reads while the other writes causing read of partially written data.
2025-09-20 23:04:16 +02:00

71 lines
2.2 KiB
TypeScript

/*
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
SPDX-License-Identifier: AGPL-3.0-or-later
*/
import * as fs from "node:fs/promises";
import type { H3Event } from "h3";
import webPush from "web-push";
import { readSubscriptions, writeSubscriptions } from "~/server/database";
let cachedVapidDetails: {
subject: string,
publicKey: string,
privateKey: string,
} | undefined;
async function useVapidDetails(event: H3Event) {
if (cachedVapidDetails) {
return cachedVapidDetails;
}
const runtimeConfig = useRuntimeConfig(event);
if (!runtimeConfig.vapidSubject) throw new Error("NUXT_VAPID_SUBJECT not set.")
if (!runtimeConfig.public.vapidPublicKey) throw new Error("NUXT_PUBLIC_VAPID_PUBLIC_KEY not set.")
if (!runtimeConfig.vapidPrivateKeyFile) throw new Error("NUXT_VAPID_PRIVATE_KEY_FILE not set.")
return cachedVapidDetails = {
subject: runtimeConfig.vapidSubject,
publicKey: runtimeConfig.public.vapidPublicKey,
privateKey: await fs.readFile(runtimeConfig.vapidPrivateKeyFile, "utf-8"),
}
}
export async function sendPush(event: H3Event, title: string, body: string) {
const vapidDetails = await useVapidDetails(event);
const payload = JSON.stringify({ title, body });
const subscriptions = readSubscriptions();
console.log(`Sending "${payload}" to ${subscriptions.length} subscribers`);
const removeIndexes = [];
for (let index = 0; index < subscriptions.length; index += 1) {
const subscription = subscriptions[index];
if (subscription.type !== "push")
continue;
try {
await webPush.sendNotification(
subscription.push as webPush.PushSubscription,
payload,
{
TTL: 3600,
urgency: "high",
vapidDetails,
}
)
} catch (err: any) {
if (err?.statusCode === 410) {
removeIndexes.push(index);
} else {
console.error("Received error sending push notice:", err.message, err?.statusCode)
console.error(err);
}
}
}
if (removeIndexes.length) {
console.log(`Removing indexes ${removeIndexes} from subscriptions`)
removeIndexes.reverse();
for (const index of removeIndexes) {
subscriptions.splice(index, 1);
}
writeSubscriptions(subscriptions);
}
console.log("Push notices sent");
}