diff --git a/src/app/mail/index.ts b/src/app/mail/index.ts index 64127ff3..1e504659 100644 --- a/src/app/mail/index.ts +++ b/src/app/mail/index.ts @@ -1,4 +1,4 @@ -import {hash, maybe, spec, throttle} from "@welshman/lib" +import {call, hash, maybe, spec, throttle} from "@welshman/lib" import type {Filter} from "@welshman/util" import {Address, DELETE, MESSAGE, makeEvent, makeHttpAuth, makeHttpAuthHeader} from "@welshman/util" import {merged} from "@welshman/store" @@ -75,50 +75,70 @@ const syncRelaySubscriptionsForEmail = ( export class MailNotifications { _controller = maybe() + // In-flight register promise, so overlapping calls (settings page submit plus + // Mail.sync) coalesce into a single PUT instead of racing two subscriptions. + _register = maybe>() + async register(email: string, frequency: string) { - const $user = User.require(app.get()) - const $signer = $user.signer + if (this._register) return this._register - if (!$signer) { - emailNotificationState.set({error: "No signer available. Check your browser extension."}) - throw new Error("No signer available") - } + this._register = call(async () => { + try { + const $user = User.require(app.get()) + const $signer = $user.signer - if (!EMAIL_NOTIFICATION_SERVER) { - emailNotificationState.set({error: "Email notification server is not configured."}) - throw new Error("Email notification server not configured") - } + if (!$signer) { + emailNotificationState.set({ + error: "No signer available. Check your browser extension.", + }) + throw new Error("No signer available") + } - let authEvent - try { - authEvent = await $signer.sign(await makeHttpAuth(EMAIL_NOTIFICATION_SERVER, "PUT")) - } catch (e) { - emailNotificationState.set({error: "Failed to create authentication. Check your signer."}) - throw new Error("Failed to sign NIP-98 auth", {cause: e}) - } + if (!EMAIL_NOTIFICATION_SERVER) { + emailNotificationState.set({error: "Email notification server is not configured."}) + throw new Error("Email notification server not configured") + } - const authHeader = makeHttpAuthHeader(authEvent) + let authEvent + try { + authEvent = await $signer.sign(await makeHttpAuth(EMAIL_NOTIFICATION_SERVER, "PUT")) + } catch (e) { + emailNotificationState.set({ + error: "Failed to create authentication. Check your signer.", + }) + throw new Error("Failed to sign NIP-98 auth", {cause: e}) + } - let res - try { - res = await fetch(buildUrl(EMAIL_NOTIFICATION_SERVER, "subscription", "email"), { - method: "PUT", - body: JSON.stringify({email, frequency, pubkey: $user.pubkey}), - headers: {Authorization: authHeader, "Content-Type": "application/json"}, - }) - } catch (e) { - emailNotificationState.set({error: `Could not reach email server. Check your connection.`}) - throw new Error("Network error", {cause: e}) - } + const authHeader = makeHttpAuthHeader(authEvent) - if (!res.ok) { - const text = await res.text().catch(() => "unknown error") - emailNotificationState.set({error: `Registration failed: ${text}`}) - throw new Error(`Registration failed: ${text}`) - } + let res + try { + res = await fetch(buildUrl(EMAIL_NOTIFICATION_SERVER, "subscription", "email"), { + method: "PUT", + body: JSON.stringify({email, frequency, pubkey: $user.pubkey}), + headers: {Authorization: authHeader, "Content-Type": "application/json"}, + }) + } catch (e) { + emailNotificationState.set({ + error: `Could not reach email server. Check your connection.`, + }) + throw new Error("Network error", {cause: e}) + } - const {key, callback} = await res.json() - emailNotificationState.set({key, callback, email, frequency, error: undefined}) + if (!res.ok) { + const text = await res.text().catch(() => "unknown error") + emailNotificationState.set({error: `Registration failed: ${text}`}) + throw new Error(`Registration failed: ${text}`) + } + + const {key, callback} = await res.json() + emailNotificationState.set({key, callback, email, frequency, error: undefined}) + } finally { + this._register = undefined + } + }) + + return this._register } async unregister() {