Commit Graph
5 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.8 38c9c52c0a fix(lists): give slow signers room to decrypt (raise timeout budget)
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]>
2026-06-16 11:18:00 -04:00
thePR0M3TH3ANandClaude Opus 4.8 0db4f0a603 perf(subscriptions): cut redundant cold-login list fetches
Two redundancies were inflating the login REQ burst (kind 30000=19 storm in
the real-env console — the cause of the slow hashtags, KNOWN_BUGS #0),
isolated with the channel-sim harness's REQ-signature + timing capture:

1. Duplicate trigger: ProfileModalController.handleAuthLogin called
   subscriptions.loadSubscriptions() AND userBlocks.loadBlocks() DIRECTLY at
   login, on top of the auth coordinator's gated list loads — a second,
   ungated relay round-trip for each. Route them through ensureLoaded()
   instead so they coalesce with the coordinator's in-flight load. Harness:
   subscriptions updateFromRelays now fires ONCE at login instead of 3×
   (≈3× fewer kind-30000 fetches in prod, where each is multiplied by the
   relay set).

2. Fetch/decrypt coupling: a decrypt retry re-ran the full relay fetch even
   though only signer availability had changed. Cache the fetched ciphertext
   (_pendingDecryptEvent) and have scheduleDecryptRetry re-decrypt it via a
   cachedDecryptEvent fast-path in updateFromRelays — no redundant relay
   query per retry.

Validated: MODE=overload (channel drops) still decrypts blocks +
subscriptions + hashtags (DMs OK); full unit suite green; subscription loop
+ aggregation tests still pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 10:26:26 -04:00
thePR0M3TH3ANandClaude Opus 4.8 10ea4f6a22 fix(subscriptions): guard the subscription-list refresh loop
The live subscription-list subscription (kind 30000) delivers our OWN list
event back through handleSubscriptionListEvent, which called updateFromRelays
on EVERY event. updateFromRelays re-fetches the list, the relay echoes the
same event through the live sub, and the handler fires again — a
self-sustaining loop that floods the cold-login burst with redundant
kind-30000 REQs (the kind 30000=19 storm in the real-env console,
KNOWN_BUGS #0). Same shape as the contact-list emit→fetch→emit loop fixed
earlier.

Add an emit-on-change guard: skip the refresh when the event is one we've
already acted on (by id — matters during the pre-decrypt login window where
subsEventCreatedAt isn't set yet) or is not newer than what we've applied.
A genuinely newer list event still refreshes exactly once.

Also extend the channel-sim harness to capture per-filter REQ signatures and
kind-30000 REQ timing, which is how this loop was isolated.

Test: tests/subscriptions-manager.test.mjs — echo of the same event id does
not re-refresh; an older event is ignored; a newer event refreshes once.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 09:53:52 -04:00
thePR0M3TH3ANandClaude Opus 4.8 1f07538c66 test(harness): extend nip07 channel-sim to all list systems
Grow the deterministic channel-simulation harness from blocks-only to a
two-session, multi-list rig so every cold-login decrypt path can be
validated locally:

- Session 1 logs in programmatically (loginWithNsec, same key the fake
  extension uses) on a HEALTHY channel and publishes the user's real
  encrypted lists via the services (block list + subscription list +
  hashtag interest) through new testHarness.seedTestLists().
- Session 2 logs in via the NIP-07 path under the target channel MODE and
  reports, per list, whether it decrypted — plus signer-ready time, the
  login REQ burst (per kind), decrypt timeouts, breaker activity, and a DM
  load probe.

testHarness: expose subscriptions + hashtag load state on getAppState().lists
and add seedTestLists() (resolves the actor from nostrClient.pubkey, which —
unlike the registry getActiveSigner() — is populated for a NIP-07 login).
Verdict is mirrored to a report file and uses process.exitCode so results
survive stdout buffering when piped/backgrounded.

Validation:
  MODE=healthy  → blocks/subscriptions/hashtags all decrypt, DMs OK.
  MODE=overload → 26 injected channel drops, ALL THREE lists still recover
                  via the transient-retry fix (db0fc079), DMs OK.
The harness exits non-zero if a list that should load doesn't, so it can
gate CI. Confirms the burst is still ~60 REQ frames at login (kind 30000
dominant) — the next target. KNOWN_BUGS #0.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 09:17:47 -04:00
thePR0M3TH3ANandClaude Opus 4.8 db0fc07989 fix(nostr): retry list decrypt on transient channel failure (not give up)
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]>
2026-06-15 09:58:31 -04:00