Skip to content

Commit 9de9afd

Browse files
Bortlesboatclaude
andcommitted
fix(nostr): clear relay publish timers to prevent leaks and unhandled rejections
publishProfileEvent() uses Promise.race with a setTimeout-based timeout promise per relay. When pool.publish() wins the race, the pending setTimeout fires later and rejects a promise nobody is listening to, causing an unhandled promise rejection in strict Node environments. Similarly, importProfileFromRelays() creates both an overall timeout timer and per-relay subscription cleanup timers that continue running even when all relays respond before the deadline. Store timer handles and clear them in finally blocks so timers are always cleaned up regardless of which side wins the race. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent aedf3ee commit 9de9afd

2 files changed

Lines changed: 19 additions & 4 deletions

File tree

extensions/nostr/src/nostr-profile-import.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,14 @@ export async function importProfileFromRelays(
111111
const events: Array<{ event: Event; relay: string }> = [];
112112

113113
// Create timeout promise
114+
let overallTimer: ReturnType<typeof setTimeout> | undefined;
114115
const timeoutPromise = new Promise<void>((resolve) => {
115-
setTimeout(resolve, timeoutMs);
116+
overallTimer = setTimeout(resolve, timeoutMs);
116117
});
117118

119+
// Track per-relay cleanup timers so they can be cleared early
120+
const subTimers: ReturnType<typeof setTimeout>[] = [];
121+
118122
// Create subscription promise
119123
const subscriptionPromise = new Promise<void>((resolve) => {
120124
let completed = 0;
@@ -151,14 +155,22 @@ export async function importProfileFromRelays(
151155
);
152156

153157
// Clean up subscription after timeout
154-
setTimeout(() => {
158+
const subTimer = setTimeout(() => {
155159
sub.close();
156160
}, timeoutMs);
161+
subTimers.push(subTimer);
157162
}
158163
});
159164

160165
// Wait for either all relays to respond or timeout
161-
await Promise.race([subscriptionPromise, timeoutPromise]);
166+
try {
167+
await Promise.race([subscriptionPromise, timeoutPromise]);
168+
} finally {
169+
clearTimeout(overallTimer);
170+
for (const t of subTimers) {
171+
clearTimeout(t);
172+
}
173+
}
162174

163175
// No events found
164176
if (events.length === 0) {

extensions/nostr/src/nostr-profile.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,10 @@ export async function publishProfileEvent(
177177

178178
// Publish to each relay in parallel with timeout
179179
const publishPromises = relays.map(async (relay) => {
180+
let timer: ReturnType<typeof setTimeout> | undefined;
180181
try {
181182
const timeoutPromise = new Promise<never>((_, reject) => {
182-
setTimeout(() => reject(new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS);
183+
timer = setTimeout(() => reject(new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS);
183184
});
184185

185186
// oxlint-disable-next-line typescript/no-floating-promises
@@ -189,6 +190,8 @@ export async function publishProfileEvent(
189190
} catch (err) {
190191
const errorMessage = err instanceof Error ? err.message : String(err);
191192
failures.push({ relay, error: errorMessage });
193+
} finally {
194+
clearTimeout(timer);
192195
}
193196
});
194197

0 commit comments

Comments
 (0)