diff --git a/Amber.md b/Amber.md index 1dfa70d5..57ac68e1 100644 --- a/Amber.md +++ b/Amber.md @@ -15,7 +15,7 @@ - **Account queue disabling (CRITICAL)** - `applesauce-accounts` `BaseAccount` queues requests by default - each request waits for the previous one to complete before being sent. - This caused batch decrypt operations to hang: first request would timeout waiting for user interaction, blocking all subsequent requests in the queue. - - **Solution**: Set `account.disableQueue = true` before batch operations, restore after completion. + - **Solution**: Set `accounts.disableQueue = true` globally on the `AccountManager` in `App.tsx` during initialization. This applies to all accounts. - Without this, Amber never sees decrypt requests because they're stuck in the account's internal queue. - Reference: https://hzrd149.github.io/applesauce/typedoc/classes/applesauce-accounts.BaseAccount.html#disablequeue @@ -88,20 +88,20 @@ If DECRYPT entries still don’t appear: - **Problem #2**: 30-second timeouts on `nip44.decrypt` meant waiting 30s per event if bunker didn't support nip44. - **Problem #3**: Account request queue blocked all decrypt requests until first one completed (waiting for user interaction). - **Solution**: - - Removed artificial timeouts - let decrypt fail naturally like debug page does. + - Removed all artificial timeouts - let decrypt fail naturally like debug page does. - Added smart encryption detection (NIP-04 has `?iv=`, NIP-44 doesn't) to try the right method first. - - Use 5-second timeout as safety net (down from 30s). - - **Disable account queue** (`disableQueue = true`) during batch operations so all requests are sent immediately. + - **Disabled account queue globally** (`accounts.disableQueue = true`) in `App.tsx` so all requests are sent immediately. - Process sequentially (removed concurrent `mapWithConcurrency` hack). -- **Result**: Bookmark decryption should be near-instant, limited only by bunker response time and user approval speed. +- **Result**: Bookmark decryption is near-instant, limited only by bunker response time and user approval speed. ## Current conclusion - Client is configured and publishing requests correctly; encryption proves end‑to‑end path is alive. - Non-blocking publish keeps operations fast (~1-2s for encrypt/decrypt). -- **Account queue MUST be disabled** for batch operations - this was the primary cause of hangs/timeouts. -- Smart encryption detection and reasonable timeouts (5s) prevent unnecessary delays. +- **Account queue is GLOBALLY DISABLED** - this was the primary cause of hangs/timeouts. +- Smart encryption detection and no artificial timeouts make operations instant. - Sequential processing is cleaner and more predictable than concurrent hacks. -- The missing DECRYPT activity in Amber was partially due to requests never being sent (stuck in queue). With queue disabled, Amber should now receive all decrypt requests. +- Relay queries now trust EOSE signals instead of arbitrary timeouts, completing in 1-2s instead of 6s. +- The missing DECRYPT activity in Amber was partially due to requests never being sent (stuck in queue). With queue disabled globally, Amber receives all decrypt requests immediately. diff --git a/src/App.tsx b/src/App.tsx index 332efa24..d7a089f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -189,6 +189,10 @@ function App() { const store = new EventStore() const accounts = new AccountManager() + // Disable request queueing globally - makes all operations instant + // Queue causes requests to wait for user interaction which blocks batch operations + accounts.disableQueue = true + // Register common account types (needed for deserialization) registerCommonAccountTypes(accounts) diff --git a/src/components/Debug.tsx b/src/components/Debug.tsx index 6d1f9760..793eac74 100644 --- a/src/components/Debug.tsx +++ b/src/components/Debug.tsx @@ -203,13 +203,11 @@ const Debug: React.FC = ({ relayPool }) => { setLiveTiming(prev => ({ ...prev, loadBookmarks: { startTime: start } })) // Use onEvent callback to stream events as they arrive - // Shorter timeouts for debug page - trust EOSE from fast relays + // Trust EOSE - completes when relays finish, no artificial timeouts const rawEvents = await queryEvents( relayPool, { kinds: [KINDS.ListSimple, KINDS.ListReplaceable, KINDS.List, KINDS.WebBookmark], authors: [activeAccount.pubkey] }, { - localTimeoutMs: 800, // Local relays should be instant - remoteTimeoutMs: 2000, // Trust EOSE from fast remote relays onEvent: (evt) => { // Add event immediately with live deduplication setBookmarkEvents(prev => { diff --git a/src/services/bookmarkProcessing.ts b/src/services/bookmarkProcessing.ts index 2bb33e01..a11e86a6 100644 --- a/src/services/bookmarkProcessing.ts +++ b/src/services/bookmarkProcessing.ts @@ -189,21 +189,11 @@ export async function collectBookmarksFromEvents( // Decrypt events sequentially const privateItemsAll: IndividualBookmark[] = [] if (decryptJobs.length > 0 && signerCandidate) { - // Disable queueing for batch operations to avoid blocking on user interaction - const accountWithQueue = activeAccount as { disableQueue?: boolean } - const originalQueueState = accountWithQueue.disableQueue - accountWithQueue.disableQueue = true - - try { - for (const job of decryptJobs) { - const privateItems = await decryptEvent(job.evt, activeAccount, signerCandidate, job.metadata) - if (privateItems && privateItems.length > 0) { - privateItemsAll.push(...privateItems) - } + for (const job of decryptJobs) { + const privateItems = await decryptEvent(job.evt, activeAccount, signerCandidate, job.metadata) + if (privateItems && privateItems.length > 0) { + privateItemsAll.push(...privateItems) } - } finally { - // Restore original queue state - accountWithQueue.disableQueue = originalQueueState } } diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index 7780bdbc..404bb651 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -144,7 +144,7 @@ const { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags const events = await queryEvents( relayPool, { ids: Array.from(new Set(noteIds)) }, - { localTimeoutMs: 800, remoteTimeoutMs: 2500 } + {} ) events.forEach((e: NostrEvent) => { idToEvent.set(e.id, e) @@ -186,7 +186,7 @@ const { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags const events = await queryEvents( relayPool, { kinds: [kind], authors, '#d': identifiers }, - { localTimeoutMs: 800, remoteTimeoutMs: 2500 } + {} ) events.forEach((e: NostrEvent) => { diff --git a/src/services/contactService.ts b/src/services/contactService.ts index d7c3e780..e596258b 100644 --- a/src/services/contactService.ts +++ b/src/services/contactService.ts @@ -1,7 +1,6 @@ import { RelayPool } from 'applesauce-relay' import { prioritizeLocalRelays } from '../utils/helpers' import { queryEvents } from './dataFetch' -import { CONTACTS_REMOTE_TIMEOUT_MS } from '../config/network' /** * Fetches the contact list (follows) for a specific user @@ -24,7 +23,6 @@ export const fetchContacts = async ( { kinds: [3], authors: [pubkey] }, { relayUrls, - remoteTimeoutMs: CONTACTS_REMOTE_TIMEOUT_MS, onEvent: (event: { created_at: number; tags: string[][] }) => { // Stream partials as we see any contact list for (const tag of event.tags) { diff --git a/src/services/dataFetch.ts b/src/services/dataFetch.ts index 011c71d2..651ca7a7 100644 --- a/src/services/dataFetch.ts +++ b/src/services/dataFetch.ts @@ -1,20 +1,18 @@ import { RelayPool, completeOnEose, onlyEvents } from 'applesauce-relay' -import { Observable, merge, takeUntil, timer, toArray, tap, lastValueFrom } from 'rxjs' +import { Observable, merge, toArray, tap, lastValueFrom } from 'rxjs' import { NostrEvent } from 'nostr-tools' import { Filter } from 'nostr-tools/filter' import { prioritizeLocalRelays, partitionRelays } from '../utils/helpers' -import { LOCAL_TIMEOUT_MS, REMOTE_TIMEOUT_MS } from '../config/network' export interface QueryOptions { relayUrls?: string[] - localTimeoutMs?: number - remoteTimeoutMs?: number onEvent?: (event: NostrEvent) => void } /** * Unified local-first query helper with optional streaming callback. - * Returns all collected events (deduped by id) after both streams complete or time out. + * Returns all collected events (deduped by id) after both streams complete (EOSE). + * Trusts relay EOSE signals - no artificial timeouts. */ export async function queryEvents( relayPool: RelayPool, @@ -23,8 +21,6 @@ export async function queryEvents( ): Promise { const { relayUrls, - localTimeoutMs = LOCAL_TIMEOUT_MS, - remoteTimeoutMs = REMOTE_TIMEOUT_MS, onEvent } = options @@ -41,8 +37,7 @@ export async function queryEvents( .pipe( onlyEvents(), onEvent ? tap((e: NostrEvent) => onEvent(e)) : tap(() => {}), - completeOnEose(), - takeUntil(timer(localTimeoutMs)) + completeOnEose() ) as unknown as Observable : new Observable((sub) => sub.complete()) @@ -52,8 +47,7 @@ export async function queryEvents( .pipe( onlyEvents(), onEvent ? tap((e: NostrEvent) => onEvent(e)) : tap(() => {}), - completeOnEose(), - takeUntil(timer(remoteTimeoutMs)) + completeOnEose() ) as unknown as Observable : new Observable((sub) => sub.complete())