perf(push): lazy load web push runtime

This commit is contained in:
Peter Steinberger
2026-04-29 08:20:50 +01:00
parent 1dd500c495
commit 9b1967e5ef
2 changed files with 25 additions and 8 deletions

View File

@@ -23,10 +23,10 @@ vi.mock("../config/paths.js", () => ({
// Stub web-push so we don't make real HTTP requests.
vi.mock("web-push", () => ({
default: {
generateVAPIDKeys: () => ({
generateVAPIDKeys: vi.fn(() => ({
publicKey: "test-public-key-base64url",
privateKey: "test-private-key-base64url",
}),
})),
setVapidDetails: vi.fn(),
sendNotification: vi.fn().mockResolvedValue({ statusCode: 201 }),
},
@@ -52,6 +52,7 @@ describe("resolveVapidKeys", () => {
const keys2 = await resolveVapidKeys(tmpDir);
expect(keys2.publicKey).toBe(keys.publicKey);
expect(keys2.privateKey).toBe(keys.privateKey);
expect(vi.mocked(webPush.generateVAPIDKeys)).toHaveBeenCalledTimes(1);
});
it("prefers env vars over persisted keys", async () => {
@@ -67,6 +68,7 @@ describe("resolveVapidKeys", () => {
expect(keys.publicKey).toBe("env-public");
expect(keys.privateKey).toBe("env-private");
expect(keys.subject).toBe("mailto:env@test.com");
expect(vi.mocked(webPush.generateVAPIDKeys)).toHaveBeenCalledTimes(1);
} finally {
delete process.env.OPENCLAW_VAPID_PUBLIC_KEY;
delete process.env.OPENCLAW_VAPID_PRIVATE_KEY;

View File

@@ -1,6 +1,5 @@
import { createHash, randomUUID } from "node:crypto";
import path from "node:path";
import webPush from "web-push";
import { resolveStateDir } from "../config/paths.js";
import { createAsyncLock, readJsonFile, writeJsonAtomic } from "./json-files.js";
@@ -41,6 +40,18 @@ const DEFAULT_VAPID_SUBJECT = "mailto:openclaw@localhost";
const withLock = createAsyncLock();
type WebPushRuntime = typeof import("web-push");
type WebPushRuntimeModule = WebPushRuntime & { default?: WebPushRuntime };
let webPushRuntimePromise: Promise<WebPushRuntime> | undefined;
async function loadWebPushRuntime(): Promise<WebPushRuntime> {
webPushRuntimePromise ??= import("web-push").then(
(mod: WebPushRuntimeModule) => mod.default ?? mod,
);
return await webPushRuntimePromise;
}
// --- Helpers ---
function resolveWebPushStatePath(baseDir?: string): string {
@@ -115,6 +126,7 @@ export async function resolveVapidKeys(baseDir?: string): Promise<VapidKeyPair>
};
}
const webPush = await loadWebPushRuntime();
const keys = webPush.generateVAPIDKeys();
const pair: VapidKeyPair = {
publicKey: keys.publicKey,
@@ -238,7 +250,7 @@ export type WebPushPayload = {
url?: string;
};
function applyVapidDetails(keys: VapidKeyPair): void {
function applyVapidDetails(webPush: WebPushRuntime, keys: VapidKeyPair): void {
webPush.setVapidDetails(keys.subject, keys.publicKey, keys.privateKey);
}
@@ -248,12 +260,14 @@ export async function sendWebPushNotification(
vapidKeys?: VapidKeyPair,
): Promise<WebPushSendResult> {
const keys = vapidKeys ?? (await resolveVapidKeys());
applyVapidDetails(keys);
const webPush = await loadWebPushRuntime();
applyVapidDetails(webPush, keys);
return sendPreparedWebPushNotification(subscription, payload);
return sendPreparedWebPushNotification(webPush, subscription, payload);
}
async function sendPreparedWebPushNotification(
webPush: WebPushRuntime,
subscription: WebPushSubscription,
payload: WebPushPayload,
): Promise<WebPushSendResult> {
@@ -300,12 +314,13 @@ export async function broadcastWebPush(
}
const vapidKeys = await resolveVapidKeys(baseDir);
const webPush = await loadWebPushRuntime();
// Set VAPID details once before fanning out concurrent sends.
applyVapidDetails(vapidKeys);
applyVapidDetails(webPush, vapidKeys);
const results = await Promise.allSettled(
subscriptions.map((sub) => sendPreparedWebPushNotification(sub, payload)),
subscriptions.map((sub) => sendPreparedWebPushNotification(webPush, sub, payload)),
);
const mapped = results.map((r, i) =>