client.js carried a stale duplicate of getActiveKey that lacked the
LEGACY: guard added to nostr/utils.js. The tombstone machinery used the
local copy, so a deleted legacy video keyed its tombstone as
ROOT:LEGACY:... while a bare zombie keyed as <pubkey>:<dTag> — the guard
never matched and the video resurrected. Import the single canonical
getActiveKey and drop the duplicate.
Also convert tests/nostr-delete-flow.test.mjs to node:test (it was
silently excluded from the runner and failing), fix its stale write-relay
config, and rewrite its subscription assertions against the real
VideoEventBuffer. New client-level regression test for the guard.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On cold login the app opened WebSockets to a user's full NIP-65 read set
(in prod often ~20 relays, most dead). The flood of failing connections
churned the main thread and starved the nip-07 extension's postMessage
round-trips, so encrypted lists (blocks, subscriptions, hashtags) were
fetched but timed out during decryption ("message channel closed",
"Decryption timed out").
Bound every read/subscribe path to MAX_SUBSCRIBE_RELAYS=8 via a shared,
test-aware capReadRelays helper, applied at the chokepoints:
- client.js applyRelayPreferences (this.relays / this.readRelays)
- relayBatchFetcher.js fetchListIncrementally (blocks, subs, hashtags)
Writes stay uncapped (publish reach preserved).
capReadRelays is user-relays-first (a user's own relays are where their
data authoritatively lives, and small fast-path limits would otherwise
crowd them out — the regression that reverted the first attempt) while
reserving RESERVED_DEFAULT_RELAY_SLOTS=2 for reliable default aggregators
so the "~20 personal relays, all dead" case still reaches a live relay.
Under test the reserved core resolves to the test-relay override, keeping
harnesses isolated.
Tests: new relay-subscribe-cap coverage (bounded + reserved-defaults +
writes-uncapped); spec-corrected user-blocks, subscriptions-manager and
nostr/client fetch tests to bounded (not exact) relay fan-out while
keeping all behavioral/security assertions (per-relay coverage, correct
decrypt, block hydration, dedup) intact. See TEST_INTEGRITY.md and
KNOWN_BUGS #0/#4. Full unit + dm:unit suites green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Profiling the live app showed the single biggest CPU cost was the relay pool
verifying every incoming event's schnorr signature synchronously on the main
thread: ~1.2s of secp256k1 work ([email protected] 855ms) during feed load and tab
switches, dropping to ~16ms when skipped. The pool was constructed as
`new SimplePool()` with no options, so nostr-tools used its default verifying
verifyEvent; the `verifyEvent: () => true` that looked like it disabled this
only existed on a mock tools object and was never wired to the real pool.
Move verification off the main thread instead of dropping it:
- Construct the pool with `verifyEvent: () => true` so it never blocks.
- Add signatureVerifyWorker (+ client) that verifies event batches in a Web
Worker and returns the valid ids; falls back to main-thread verification only
when Workers are unavailable (never silently trusts).
- VideoEventBuffer verifies each batch via the worker before committing, and
drops events that fail (fails closed). The verifier is injectable so tests can
virtualize the worker; subscribeVideos passes it through and returns the flush
promise from EOSE so commits are awaitable.
Tests: new signatureVerifyWorkerClient.test.mjs (worker protocol + drop-invalid
+ empty/no-id handling); videoEventBuffer.test.mjs and client.test.mjs adapted
to the async, verified commit path with a passthrough verifier.
Note: tests/nostr-delete-flow.test.mjs has a PRE-EXISTING failure on
`should not trigger subscription callbacks` (fails on clean unstable, and is a
standalone script not run by the unit-test runner); its edits here only adapt it
to the now-async flush.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replaced the deprecated `fetchVideos` implementation with a wrapper around the newer `subscribeVideos` method. This reduces code duplication and ensures consistent behavior for video fetching logic. The wrapper maintains the original Promise-based API and ensures NIP-71 metadata is populated before resolving.
Added tests in `tests/nostr/client.test.mjs` to verify `fetchVideos` behavior.
Co-authored-by: PR0M3TH3AN <[email protected]>
Updates `NostrClient.subscribeVideos` to buffer incoming events and invoke the `onVideo` callback once per flush cycle with an array of updates, instead of once per video. This significantly reduces redundant sorting, filtering, and persistence operations in `NostrService`.
Also optimizes `populateNip71MetadataForVideos` to run as a single batched request per flush cycle.
BREAKING CHANGE: `subscribeVideos` callback now receives `Video[]` instead of `Video`. All internal consumers have been updated.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Updated `NostrClient.fetchListIncrementally` to accept a `since` override.
- Updated `HashtagPreferencesService.load` to pass `since` based on local cache timestamp.
- Ensures preferences are refreshed if local state is stale or missing, overriding potentially incorrect `SyncMetadataStore` state.
- Added regression tests for `fetchListIncrementally` since parameter logic.
Co-authored-by: PR0M3TH3AN <[email protected]>