Same class of bug as the subscription fix, applied everywhere an idempotent,
replaceable list is published with an optimistic UI: when no relay ACKs within the
10s window (slow/flaky relays), the event was still sent and almost always
persists (it shows up after a refresh), but assertAnyRelayAccepted threw -> the UI
showed a scary error / reverted, even though it worked.
- nostrPublish.js: add shared helpers relayPublishFailuresAreAllTimeouts() and
assertAnyRelayAcceptedOrUnconfirmed() — the latter returns a summary flagged
{ unconfirmed: true } on all-timeout instead of throwing; explicit relay
rejections (and pre-publish signer/encryption errors) still throw.
- Applied to the replaceable-list publishes: subscriptions (kind 30000, refactored
to the shared helper), user mute/block list (10000), relay list (10002), DM
relay hints (10050), hashtag preferences (30015).
Left as-is for now (higher-stakes, separate follow-up): the generic content
publish path (video/edit/delete/revert/rebroadcast) in publishHelpers/client.js —
same latent issue but the video outcome-describer needs careful handling.
Tests: tests/subscription-publish-timeout.test.mjs (8) now cover both the
classifier and assertAnyRelayAcceptedOrUnconfirmed (confirmed->summary,
all-timeout->soft, rejection->throws). hashtag-preferences suite green (14).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The cold-login REQ storm came from list subsystems subscribing/fetching across
the user's full NIP-65 relay set (often ~20) instead of the documented §17 read
cap. The fetchListIncrementally paths already route through capReadRelays, but
the live subscriptions and direct pool.list calls bypassed it:
- userBlocks.ensureBlockListSubscription: cap the live block-list subscription
- subscriptions.ensureSubscriptionListSubscription: cap the live follows-list
subscription
- hashtagPreferencesService: cap the relay set used by pool.list + fetch
capReadRelays keeps the user's own relays first and guarantees the reserved
defaults, bounded to MAX_SUBSCRIBE_RELAYS (8). Trade-off accepted: a list event
living only on a user's 9th+ relay may sync on a later pass rather than at login.
Regression: tests/user-blocks-relay-cap.test.mjs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
File-size decomposition (no behavior change). Move the state-free TinyEventEmitter
and the decrypt-retry helpers (isTransientDecryptError, computeRetryDelay + their
transient-error tables and MAX_DECRYPT_RETRY_DELAY_MS) into js/userBlocksHelpers.js.
userBlocks.js: 2520 -> 2443 lines (back under its 2478 limit).
Build + full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Under a NIP-46 remote signer, every decrypt is a PUBLISHED relay RPC. The list
services (blocks/subscriptions/hashtags) probed all decryptor families
concurrently via Promise.any — so each list event published nip04 AND nip44 RPCs
at once. On login that flooded the relay ("rate-limited: you are noting too
much"), and the racing/queued RPCs blew past the 30s decrypt budget → the
"Decryption timed out" failures.
A NIP-04 payload carries the "?iv=" marker and can ONLY be decrypted by nip04;
otherwise it's nip44. The other family can never succeed on a given ciphertext,
so racing it is pure waste (and a wasted published RPC under NIP-46). Restrict
the decrypt probe to the family matching the ciphertext format (fall back to all
if the format is somehow unknown). Halves cold-load RPCs and stops the racing
publishes that tripped the rate limit; benefits all signer types.
Spec correction: the hashtag "retries decryptors when hinted scheme fails" test
stubbed an impossible scenario (the same ciphertext decrypting under nip04 after
nip44 failed). Rewritten to assert the real contract — the ciphertext format
routes decryption and overrides a misleading `encrypted` tag, invoking only the
matching decryptor.
test_integrity_note:
change_type: ["spec_correction"]
scenarios:
- id: SCN-decrypt-format-routing
given: "a nip04-format ciphertext (?iv=) tagged misleadingly as nip44"
when: "a list service decrypts it"
then: "only the nip04 decryptor is invoked; nip44 is never attempted"
anti_cheat_rationale:
prevents: ["over-mocking internal logic", "asserting impossible cross-family decryption"]
relaxation:
did_relax_any_assertion: false
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A raw window.nostr probe in the user's browser (getPublicKey → nip04.encrypt
→ nip04.decrypt, bypassing ALL of bitvid) HUNG forever, with the extension's
own console showing `ObjectMultiplex - orphaned data for "background-liveness"`
+ `malformed chunk` + listener-leak warnings. That is the NIP-07 extension's
content-script ↔ background-service-worker channel being dead (classic MV3
worker death) — an extension/environment failure, not a bitvid bug. No client
change can force an unresponsive signer to answer; the fix is to reload/unlock
the extension.
What bitvid CAN do is stop failing silently. Add js/utils/signerHealthNotice.js:
after a few consecutive list-decrypt timeouts while the signer reports itself
present, emit ONE rate-limited, actionable user notice ("your signer isn't
responding to decryption — unlock/reload the extension and refresh") instead of
the silent 15-minute retry loop. Reset on any decrypt success so a transient
blip doesn't warn. Wired into blocks, hashtags, and subscriptions.
Also documents the definitive root-cause + all resilience increments in
KNOWN_BUGS #0. Test: tests/signer-health-notice.test.mjs (threshold, rate-limit,
success-reset). Full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-env console: signerStatus "present" but every block/hashtag decrypt
timed out at the 15s service budget, forever, retrying every ~15s for
15+ minutes — lists never loaded.
Reproduced deterministically with the channel-sim harness (latency
override): with per-call latency BELOW the service budget all lists load;
the instant it crosses the budget every list fails (NO) and retries
forever. A nip-07 decrypt is a postMessage round-trip to the extension's
single-threaded, possibly-sleeping service worker — under cold-login load
real extensions take 15-25s to answer even though the crypto is instant.
The earlier 6s per-call timeout was killing those in-progress decrypts.
Raise the budget across blocks/hashtags/subscriptions so a slow-but-
responsive signer completes instead of timing out:
- service-level DECRYPT_TIMEOUT_MS 15s → 30s
- per-call nip-07 decrypt timeout 6s → 25s interactive / ~10-12s background
- backoff cap 30s → 60s so a persistently dead signer settles into an
occasional heartbeat instead of pinning the extension every ~15s
Harness: at 16s/call (previously NO across the board) blocks +
subscriptions + hashtags now all decrypt (0 timeouts); healthy mode
unchanged (signer-ready ~1.2s). Full unit suite green. KNOWN_BUGS #0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Built a deterministic NIP-07 channel-simulation harness
(scripts/perf/nip07-channel-sim.mjs) that reproduces the real cold-login
failure locally with a fake extension whose message channel drops under
concurrent load (MODE=overload), recovers (MODE=slow), or stays dead. It
seeds a real encrypted block list and reports whether it decrypts.
The harness pinned the actual root cause that the relay/breaker/timeout
work hadn't fixed: when the channel briefly drops during the login burst,
the per-scheme decrypt probe (Promise.any) rejects with
"message channel closed" — and every list service SWALLOWED that and
treated it as "the user has no blocks / no subs / no hashtags", or routed
it to a give-up branch. Either way: no retry, so the list stayed empty
until a full page refresh, even though the channel recovered seconds later.
Fix: classify transient failures (channel-death messages, breaker fast-fail
code, our own decrypt-timeout) and route them to the existing keep-stale +
scheduleDecryptRetry path instead of swallowing/abandoning:
- userBlocks: re-throw a transient sub-error out of the Promise.any probe
(was silently returning [] = "no blocks"); route it (and channel/breaker
errors) through the retry branch.
- hashtagPreferencesService + subscriptions: broaden the retry condition
from the timeout code only to any transient error, recursing into the
nested Promise.any AggregateError causes.
Harness verdict (MODE=overload, 16 channel drops): block list
BLOCK LIST DECRYPTED NO -> YES after the fix; signer-ready ~1.9s.
Adds getAppState().lists observability to the test harness. Full unit
suite green. KNOWN_BUGS #0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-env console (2026-06-15) showed [signer-ready] taking ~48s and every
list decrypt timing out before AND after it. Root mechanism: a priority
inversion on the 2-slot NIP-07 request queue.
- List decrypts run at NIP07_PRIORITY.HIGH; the readiness gate's
permission/enable handshake ran at NORMAL. So a flood of HIGH-priority
decrypts jumped ahead of the gate and starved it — the channel never got
warmed, so the decrypts kept failing. Fix: add NIP07_PRIORITY.CRITICAL
(above HIGH) and run the permission/enable handshake at CRITICAL so it
always wins queue ordering.
- Login-time decrypt timeouts were 12-15s even though a healthy
nip04/nip44 decrypt is local crypto (~1s); the long window was only to
tolerate a first-grant prompt, which the gate already pre-grants. A 15s
timeout just monopolizes one of the two queue slots on a doomed call,
blocking the gate for 15s at a time. Cut blocks/hashtags/subscriptions
login decrypt timeouts to 6s so slots cycle ~2.5x faster (and the circuit
breaker trips in ~18s instead of ~45s). Service-level 15s wrappers stay
as a backstop (now strictly longer than the nip07 layer).
Part of KNOWN_BUGS #0 ("cut the burst, then add a safety net"). Full unit
suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
kind-10000/30002 block-list live subscription now goes through L1 (dedup +
reconnect re-issue), handle closed on teardown; relaySubscriptionService kept as
manager-less fallback. user-blocks + dm-block-filter suites pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- `handleVisibility` only scheduled intervals but didn't trigger an immediate refresh when returning to the tab.
- This caused `tests/unit/services/exploreDataService.test.mjs` to fail (assertion `0 !== 1`).
- Added explicit calls to `refreshWatchHistoryTagCounts` and `refreshTagIdf` when `document.hidden` becomes false.
Co-authored-by: PR0M3TH3AN <[email protected]>
Removes unused imports `normalizeNostrPubkey`, `CACHE_POLICIES`, and `STORAGE_TIERS` from `js/userBlocks.js` to improve code cleanliness.
This change does not affect functionality as these imports were not used in the file.
Verified with lint checks and by running `tests/user-blocks.test.mjs` (which has a known unrelated failure).
Co-authored-by: PR0M3TH3AN <[email protected]>
The NIP-07 adapter uses type "nip07" but all permission pre-granting
checks only matched type "extension", causing NIP-07 browser extension
users to fail decryption for block/mute lists, subscriptions, DMs, and
watch history. Hashtag preferences appeared to work because their read
path had a separate fix, masking the systemic issue.
Changes:
- shouldRequestExtensionPermissions() now matches both "extension" and
"nip07" signer types (central fix affecting DM signals, comments,
publish helpers, and watch history fetch)
- buildDmDecryptContext() pre-grants extension permissions for NIP-07
- userBlocks.js: remove capabilities-gated decryptor check that failed
with lazy NIP-07 module injection (same fix already in subscriptions.js);
always resolve signer regardless of allowPermissionPrompt; increase
fast-path relay timeout from 6s to 10s
- watchHistoryService: recognize NIP-07 type and always resolve signer
- All publish paths (blocks, subscriptions, hashtags, relays, uploads,
sharing, moderation) now pre-grant permissions for NIP-07 signers
https://claude.ai/code/session_01PipsdaTQmw7wBTJ1LmJjkk
- Introduce `js/constants.js` with semantic time constants (e.g., `ONE_MINUTE_MS`, `STANDARD_TIMEOUT_MS`) and feature-specific TTLs.
- Export `KIND_MUTE_LIST` from `js/nostrEventSchemas.js` to standardize NIP-51 mute list kind usage.
- Refactor `js/state/cache.js`, `js/nostr/cachePolicies.js`, `js/channelProfile.js`, and various services to use centralized constants instead of hardcoded magic numbers.
- Update `js/nostr/relayConstants.js` to derive values from the shared time constants.
- Replace duplicate definitions of timeouts and retry delays across the codebase.
Co-authored-by: PR0M3TH3AN <[email protected]>
The NIP-07 login flow was extremely slow because list decryption (blocks,
subscriptions, hashtag preferences) started before the signer was ready.
On page refresh, all three services failed with "permission-required" and
scheduled 3-second retry delays, adding 6-10 seconds to every login.
Key fixes:
- List loading now waits for the permission pre-grant to complete before
attempting decryption, ensuring the signer is available on first try
- Extension wait timeout increased from 3s to 5s for slower extensions
- Decrypt timeouts reduced from 20s to 10s (signer is guaranteed ready)
- Auto-login now forwards authType/providerId from saved profiles
- Storage tab unlock falls back to ensureActiveSignerForPubkey when the
global signer is not yet registered
- Watch history feed registration adds defensive re-registration and
try/catch fallback to prevent "Feed not registered" errors
- Decryption scheme cache TTL extended from 10 minutes to 2 hours
- ensureExtensionPermissions waits for extension injection when there
are outstanding permissions to request (was returning immediately)
https://claude.ai/code/session_01T4wwPu7m5qp1Nxx3H2v57j
Three performance bottlenecks were causing slow list loading after NIP-07 login:
1. Each list service (blocks, subscriptions, hashtag preferences) maintained
its own isolated decryptionSuccessCache. When blocks decrypted with nip44_v2,
subscriptions and hashtag preferences had no way to know — they each had to
independently probe all schemes through the NIP-07 extension queue, tripling
the number of extension calls during the critical login path.
2. List loading was gated on both relaysReadyPromise AND permissionPromise in
the coordinator. Since the NIP-07 extension already grants decrypt permissions
during loginWithExtension(), the redundant pre-grant was adding 2-5 seconds
of unnecessary latency before any list fetching could begin.
3. The DECRYPT_RETRY_DELAY_MS was 10 seconds across all services. When a decrypt
attempt failed transiently (extension still initializing), the retry waited
far too long before trying again.
Changes:
- Create shared decryptionSchemeCache module used by all three list services
- Add fast-path: when shared cache knows the correct scheme, try ONLY that
scheme before falling back to parallel Promise.any() probe
- Remove permissionPromise from the list-loading gate in authSessionCoordinator
- Reduce DECRYPT_RETRY_DELAY_MS from 10s to 3s in all three services
- Clear shared cache on logout
https://claude.ai/code/session_013QrGMnxtMTsGTPqaP35jMj
Login with NIP-07 extensions was extremely slow because list loading
(blocks, subscriptions, hashtag preferences) happened sequentially and
each list's decryption tried encryption schemes one at a time.
Changes:
- Load blocks, subscriptions, and hashtags in parallel instead of
sequentially (blocks no longer blocks other lists from starting)
- Use Promise.any() to try all decryption schemes simultaneously
instead of trying nip44_v2 → nip44 → nip04 one-by-one
- Deduplicate same-family schemes (nip44/nip44_v2) to avoid redundant
extension calls
- Increase NIP-07 request queue concurrency from 3 to 5 to support
parallel decrypt operations during login
- Raise no-prompt decrypt timeout from 3s/5s to 8s since extensions
that have already granted permission still need time to decrypt
- Increase feed sync timeout from 8s to 12s to give lists more time
to settle before rendering unfiltered content
https://claude.ai/code/session_01V6m1gTecsppPrBHQnUyPnF
Increases the background decryption timeout for User Block Lists from 5s to 30s and raises the NIP-07 request priority from LOW to NORMAL. This resolves issues where slower extensions or busy queues caused decryption to time out, leading to stale block lists and feed updates.
- Bump `DECRYPT_TIMEOUT_MS` and `BACKGROUND_DECRYPT_TIMEOUT_MS` to 30000ms.
- Use `NIP07_PRIORITY.NORMAL` instead of `LOW` for decryption requests.
Co-authored-by: PR0M3TH3AN <[email protected]>
Refactored `js/userBlocks.js` to handle NIP-07 decryption timeouts gracefully.
Previously, if a background decryption timed out (common with busy extensions or queues), the user's block list was reset to an empty state, exposing them to blocked content until the next successful retry.
The fix ensures that if a timeout occurs, the existing (stale) block list is preserved while a retry is scheduled in the background.
Also increased the `BACKGROUND_DECRYPT_TIMEOUT_MS` from 2500ms to 5000ms to reduce false positives.
Added `tests/user-blocks-decryption.test.mjs` to verify the fix and audit the decryption flow.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Updated `StorageService` to check `signer.capabilities` before calling encryption methods, ensuring graceful fallback from NIP-44 to NIP-04.
- Updated `UserBlockList` to check `signer.capabilities` before registering decryptors, preventing "Decryption timed out" errors caused by queueing unsupported method calls.
- Fixed flakiness in `tests/user-blocks.test.mjs` by adding delays to allow background promises to settle between tests.
- Verified changes with reproduction test case and existing test suite.
Co-authored-by: PR0M3TH3AN <[email protected]>
Prioritizes fetching standard NIP-51 (Kind 10000) mute lists with a fail-fast timeout (6s) and applies them immediately upon receipt. Legacy block lists (Kind 30002, Kind 10000 d=user-blocks) are now fetched in the background without blocking the UI initialization.
This resolves an issue where slow relays handling legacy queries could cause the entire block list loading process to hang for minutes.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Updates `js/userBlocks.js` to write block entries to a standard Kind 10000 Mute List event.
- Implements a hybrid storage model: unencrypted public mutes are stored in `p` tags, while private blocks are stored in the encrypted event content.
- Maintains read compatibility with legacy `d=user-blocks` lists (both Kind 10000 and Kind 30002) by merging and prioritizing the newest event timestamp.
- Updates local caching to persist both public mutes and private blocks, preventing data loss or "zombie" entries on reload.
- Updates `js/subscriptions.js` to correctly detect NIP-44 capabilities on `Nip07Adapter` signers, fixing a regression where NIP-04 fallback was skipped.
- Updates `js/nostrEventSchemas.js` to mark `USER_BLOCK_LIST` as legacy and define `MUTE_LIST`.
- Updates unit tests to verify the new multi-kind fetching and merged publishing logic.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Updates `js/userBlocks.js` to treat Kind 10000 (standard Mute List) as the primary write target.
- Public mutes are stored in `p` tags, and private blocks are stored in encrypted content within the same event.
- Legacy `d=user-blocks` lists are still read for backward compatibility (preferring the newest event), but no longer written.
- Updates `js/nostrEventSchemas.js` to mark `USER_BLOCK_LIST` as legacy and standardise `MUTE_LIST`.
- Updates UI controllers (`MoreMenuController`, `ProfileModalController`) to use "Mute/Block List" terminology and delegate all mute/block actions to `userBlocks`.
- Updates `ModerationService` to delegate viewer mute operations to `userBlocks` and disable direct Kind 10000 publishing to prevent overwrites.
- Documentation updated in `docs/nostr-event-schemas.md`.
- Fixes regressions in `more-menu-controller.test.mjs`, `moderation-service.test.mjs`, `trusted-mute-lists.test.mjs`, and `user-blocks.test.mjs`.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Reduced decryption timeout for user lists from 90s to 15s to fail faster.
- Optimized NIP-07 decryption calls to avoid retries and long timeouts during critical loading paths.
- Fixed a bug where `loginWithExtension` triggered `getPublicKey` twice, doubling the user interaction time.
- Added a 10s grace period for relay health warnings to prevent false positives on startup.
- Verified `deferBlocks` logic in `authService` prevents blocking the login UI on slow list decryption.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Fixes issue where user lists (subscriptions, blocks) fail to load if local cache is stale but metadata store is fresh.
- Updates `js/userBlocks.js` and `js/subscriptions.js` to persist `createdAt` timestamp and use it for incremental fetch `since` parameter.
- Optimizes `js/nostr/client.js` `ensureActiveSignerForPubkey` to reduce redundant `getPublicKey` calls to NIP-07 extension by 50%.
- Adds `preloadedPubkey` support to `createNip07Adapter`.
- Adds reproduction test for NIP-07 concurrency issue.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Implements a priority queue for NIP-07 (browser extension) requests to allow critical tasks (blocklist/prefs decryption) to jump the line.
- Updates NIP-46 client and adapters to support priority overrides for encryption/decryption.
- Updates UserBlockListManager, SubscriptionsManager, and HashtagPreferencesService to request HIGH priority when decrypting data.
- Ensures strict serialization for NIP-07 to prevent extension errors while optimizing latency for critical data.
Co-authored-by: PR0M3TH3AN <[email protected]>
Introduced a request queue in `js/nostr/nip07Permissions.js` to serialize all interactions with the NIP-07 extension (`window.nostr`).
Updated `js/userBlocks.js`, `js/services/hashtagPreferencesService.js`, and `js/auth/signingAdapter.js` to wrap direct extension calls with `runNip07WithRetry`, ensuring they participate in the queue.
This resolves "message channel closed" errors and timeouts caused by concurrent requests flooding the extension during sync operations.
Co-authored-by: PR0M3TH3AN <[email protected]>
Changes `loadBlocks` to fetch `kind: 10000` with `d=user-blocks` instead of `kind: 30002` for block lists, while keeping the generic `kind: 10000` fetch for mute lists.
Updates `applyEvents` to separate block lists (tagged or legacy 30002) from mute lists (untagged 10000) and merge them instead of prioritizing one over the other.
Updates tests to reflect the merged behavior.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Updated `NostrClient.fetchListIncrementally` to prioritize user's NIP-65 read relays, cap concurrent requests, and temporarily mark failing relays as unreachable.
- Added `markRelayUnreachable` to `NostrClient` for temporary relay exclusion.
- Updated `SubscriptionsManager`, `HashtagPreferencesService`, and `UserBlockListManager` to use `relayManager.getReadRelayUrls()` for selecting relays, ensuring consistency with user preferences.
- Added structured logging for relay selection in `fetchListIncrementally`.
- Updated unit tests to mock `relayManager` behavior correctly.
Co-authored-by: PR0M3TH3AN <[email protected]>