Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions src/common/db/indexeddb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from './types.js';

let dbInstance: IDBDatabase | null = null;
const OPEN_DB_TIMEOUT_MS: number = 3000;

/**
* Opens or retrieves the cached IndexedDB connection
Expand All @@ -17,12 +18,40 @@ export async function openDb(): Promise<IDBDatabase> {
}

return new Promise<IDBDatabase>((resolve, reject) => {
let settled: boolean = false;
let timeoutId: number | null = null;

const finishResolve = (db: IDBDatabase): void => {
if (settled) {
return;
}
settled = true;
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
resolve(db);
};

const finishReject = (error: Error): void => {
if (settled) {
return;
}
settled = true;
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
reject(error);
};

if (typeof indexedDB === 'undefined') {
reject(new Error('IndexedDB not available'));
finishReject(new Error('IndexedDB not available'));
return;
}

const request = indexedDB.open(DB_NAME, DB_VERSION);
timeoutId = window.setTimeout((): void => {
finishReject(new Error('IndexedDB open timed out'));
}, OPEN_DB_TIMEOUT_MS);

request.onupgradeneeded = (event): void => {
const db = request.result;
Expand Down Expand Up @@ -84,18 +113,21 @@ export async function openDb(): Promise<IDBDatabase> {
request.onsuccess = (): void => {
dbInstance = request.result;
console.log('[IndexedDB] Database opened successfully');
resolve(dbInstance);
finishResolve(dbInstance);
};

request.onerror = (): void => {
console.error('[IndexedDB] Failed to open database', request.error);
reject(request.error || new Error('Failed to open IndexedDB'));
finishReject(request.error || new Error('Failed to open IndexedDB'));
};

request.onblocked = (): void => {
console.warn(
'[IndexedDB] Database upgrade blocked by another connection',
);
finishReject(
new Error('IndexedDB open blocked by another active connection'),
);
};
});
}
Expand Down
47 changes: 42 additions & 5 deletions src/common/relay-socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { getSessionPrivateKey } from './session.js';

const RELAY_AUTH_PERMISSIONS_KEY: string = 'nostr_relay_auth_permissions_v1';
const SHARED_RELAY_CONNECT_TIMEOUT_MS: number = 5000;

type RelayAuthPermission = 'allow' | 'deny';

Expand Down Expand Up @@ -400,29 +401,65 @@ async function ensureSharedRelaySocket(
const socket: WebSocket = new WebSocket(connection.relayUrl);
connection.socket = socket;
attachSharedRelayListeners(connection);
let settled: boolean = false;

const finishResolve = (): void => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeoutId);
connection.openPromise = null;
resolve(socket);
};

const finishReject = (error: Error): void => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeoutId);
connection.openPromise = null;
reject(error);
};

const timeoutId = window.setTimeout((): void => {
try {
socket.close();
} catch {
// Best-effort cleanup for stalled connections.
}
finishReject(
new Error(
`Timed out connecting to relay ${connection.relayUrl}`,
),
);
}, SHARED_RELAY_CONNECT_TIMEOUT_MS);

socket.addEventListener(
'open',
(): void => {
connection.openPromise = null;
resolve(socket);
finishResolve();
},
{ once: true },
);

socket.addEventListener(
'error',
(): void => {
connection.openPromise = null;
reject(new Error(`Failed to connect to relay ${connection.relayUrl}`));
finishReject(
new Error(`Failed to connect to relay ${connection.relayUrl}`),
);
},
{ once: true },
);

socket.addEventListener(
'close',
(): void => {
connection.openPromise = null;
finishReject(
new Error(`Relay ${connection.relayUrl} closed before opening`),
);
},
{ once: true },
);
Expand Down
13 changes: 10 additions & 3 deletions src/common/timeline-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ export async function loadTimeline(options: LoadTimelineOptions): Promise<void>
let finalized: boolean = false;
let clearedPlaceholder: boolean =
options.output.querySelectorAll('.event-container').length > 0;
let subscription: Subscription | null = null;
let mainTimeoutId: number | null = null;

const isInitialLoad: boolean = currentUntilTimestamp >= Date.now() / 1000 - 60;
const originalUntilTimestamp: number = currentUntilTimestamp;
Expand Down Expand Up @@ -485,6 +487,11 @@ export async function loadTimeline(options: LoadTimelineOptions): Promise<void>
}
finalized = true;

if (mainTimeoutId !== null) {
clearTimeout(mainTimeoutId);
mainTimeoutId = null;
}

flushBufferedEvents();
persistBufferedTimeline();

Expand All @@ -498,6 +505,7 @@ export async function loadTimeline(options: LoadTimelineOptions): Promise<void>
options.connectingMsg.style.display = 'none';
}

subscription?.unsubscribe();
connectionSub?.unsubscribe();
options.onFinalize?.(getContext());
};
Expand All @@ -514,7 +522,6 @@ export async function loadTimeline(options: LoadTimelineOptions): Promise<void>
activeTimeouts.push(timeoutId);
};

let subscription: Subscription | null = null;
subscription = rxNostr.use(req, { relays }).subscribe({
next: (packet: EventPacket): void => {
if (!routeIsActive()) {
Expand Down Expand Up @@ -571,9 +578,9 @@ export async function loadTimeline(options: LoadTimelineOptions): Promise<void>

req.emit(filter);

const timeoutId = window.setTimeout((): void => {
mainTimeoutId = window.setTimeout((): void => {
options.onTimeout?.(getContext());
finalizeLoading();
}, timeoutMs);
activeTimeouts.push(timeoutId);
activeTimeouts.push(mainTimeoutId);
}