mirror of
https://github.com/block/buzz.git
synced 2026-11-02 15:31:27 +00:00
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
import { classifyRelayClosed } from "@/shared/api/relayClosedPolicy";
|
|
import {
|
|
activateRateLimit,
|
|
parseRateLimitHint,
|
|
rateLimitRemainingMs,
|
|
} from "@/shared/api/relayRateLimitGate";
|
|
import {
|
|
sortEvents,
|
|
type RelaySubscription,
|
|
type RelaySubscriptionFilter,
|
|
} from "@/shared/api/relayClientShared";
|
|
import type { RelayEvent } from "@/shared/api/types";
|
|
|
|
const RETRY_BASE_DELAY_MS = 1_000;
|
|
const RETRY_MAX_DELAY_MS = 30_000;
|
|
|
|
type LiveSubscription = Extract<RelaySubscription, { mode: "live" }>;
|
|
|
|
export function clearClosedRetry(subscription: LiveSubscription) {
|
|
if (subscription.closedRetryTimeout === undefined) return;
|
|
window.clearTimeout(subscription.closedRetryTimeout);
|
|
subscription.closedRetryTimeout = undefined;
|
|
}
|
|
|
|
export function handleRelayClosed({
|
|
subscriptions,
|
|
subId,
|
|
message,
|
|
sendReq,
|
|
}: {
|
|
subscriptions: Map<string, RelaySubscription>;
|
|
subId: string;
|
|
message: string;
|
|
sendReq: (subId: string, filter: RelaySubscriptionFilter) => Promise<void>;
|
|
}) {
|
|
const subscription = subscriptions.get(subId);
|
|
if (!subscription) return;
|
|
if (subscription.mode === "history") {
|
|
// Classify before rejecting so a `rate-limited:` history CLOSED arms the
|
|
// gate for concurrent ops. A history sub can't be retried (the caller holds
|
|
// the promise), so we still reject immediately after arming.
|
|
const closedClass = classifyRelayClosed(message);
|
|
if (closedClass === "rate-limited") {
|
|
const hintSeconds = parseRateLimitHint(message);
|
|
activateRateLimit(hintSeconds);
|
|
}
|
|
window.clearTimeout(subscription.timeout);
|
|
subscriptions.delete(subId);
|
|
subscription.reject(
|
|
new Error(message || "Relay closed the history subscription."),
|
|
);
|
|
return;
|
|
}
|
|
recoverLiveSubscriptionFromClosed({
|
|
subscriptions,
|
|
subId,
|
|
subscription,
|
|
message,
|
|
sendReq,
|
|
});
|
|
}
|
|
|
|
function recoverLiveSubscriptionFromClosed({
|
|
subscriptions,
|
|
subId,
|
|
subscription,
|
|
message,
|
|
sendReq,
|
|
}: {
|
|
subscriptions: Map<string, RelaySubscription>;
|
|
subId: string;
|
|
subscription: LiveSubscription;
|
|
message: string;
|
|
sendReq: (subId: string, filter: RelaySubscriptionFilter) => Promise<void>;
|
|
}) {
|
|
subscription.resolveReady?.();
|
|
subscription.resolveReady = undefined;
|
|
|
|
const closedClass = classifyRelayClosed(message);
|
|
|
|
if (closedClass === "terminal") {
|
|
// Auth/access/filter failure — permanently remove the subscription so it
|
|
// doesn't silently loop.
|
|
subscriptions.delete(subId);
|
|
return;
|
|
}
|
|
|
|
if (subscription.closedRetryTimeout !== undefined) return;
|
|
|
|
const attempt = subscription.closedRetryAttempt ?? 0;
|
|
const backoffMs = Math.min(
|
|
RETRY_BASE_DELAY_MS * 2 ** attempt,
|
|
RETRY_MAX_DELAY_MS,
|
|
);
|
|
|
|
let delayMs = backoffMs;
|
|
|
|
if (closedClass === "rate-limited") {
|
|
// Activate the gate so concurrent operations back off too.
|
|
const hintSeconds = parseRateLimitHint(message);
|
|
activateRateLimit(hintSeconds);
|
|
// Use the gate's actual remaining time so a shorter hint arriving under a
|
|
// longer active gate does not schedule a premature retry that just gets
|
|
// another CLOSED. The fallback covers the gate-inactive edge case
|
|
// (hint * 1000, or 10s default when no hint).
|
|
const fallbackMs = (hintSeconds ?? 10) * 1_000;
|
|
delayMs = Math.max(backoffMs, rateLimitRemainingMs() || fallbackMs);
|
|
}
|
|
|
|
subscription.closedRetryAttempt = attempt + 1;
|
|
subscription.closedRetryTimeout = window.setTimeout(() => {
|
|
subscription.closedRetryTimeout = undefined;
|
|
if (subscriptions.get(subId) !== subscription) return;
|
|
void sendReq(subId, subscription.filter).catch((error) => {
|
|
if (subscriptions.get(subId) !== subscription) return;
|
|
console.error("Failed to restore closed relay subscription", error);
|
|
recoverLiveSubscriptionFromClosed({
|
|
subscriptions,
|
|
subId,
|
|
subscription,
|
|
message,
|
|
sendReq,
|
|
});
|
|
});
|
|
}, delayMs);
|
|
}
|
|
|
|
export function prepareSubscriptionEvent(
|
|
subscription: RelaySubscription,
|
|
event: RelayEvent,
|
|
) {
|
|
if (subscription.mode === "history") {
|
|
subscription.events.push(event);
|
|
return false;
|
|
}
|
|
subscription.closedRetryAttempt = 0;
|
|
clearClosedRetry(subscription);
|
|
subscription.lastSeenCreatedAt = Math.max(
|
|
subscription.lastSeenCreatedAt ?? 0,
|
|
event.created_at,
|
|
);
|
|
return true;
|
|
}
|
|
|
|
export function handleSubscriptionEose({
|
|
subscriptions,
|
|
subId,
|
|
closeSubscription,
|
|
}: {
|
|
subscriptions: Map<string, RelaySubscription>;
|
|
subId: string;
|
|
closeSubscription: (subId: string) => Promise<void>;
|
|
}) {
|
|
const subscription = subscriptions.get(subId);
|
|
if (!subscription) return;
|
|
if (subscription.mode === "live") {
|
|
subscription.resolveReady?.();
|
|
subscription.resolveReady = undefined;
|
|
subscription.closedRetryAttempt = 0;
|
|
clearClosedRetry(subscription);
|
|
return;
|
|
}
|
|
window.clearTimeout(subscription.timeout);
|
|
subscriptions.delete(subId);
|
|
void closeSubscription(subId);
|
|
subscription.resolve(sortEvents(subscription.events));
|
|
}
|