P1 foundation of the data-flow refactor (docs/architecture-refactor.md):
- js/nostr/subscriptionManager.js: the single chokepoint for relay reads.
Builds on relaySubscriptionService (dedup by key) and adds: health-gated
default relays, deduped one-shot list() with timeout, and a reconnect
registry that re-issues active subs centrally (instead of every subsystem
re-subscribing on reconnect — the storm amplifier). Callers pass batched
filters (one REQ for many ids), never per-item.
- Export RelaySubscriptionService class for test isolation.
- scripts/check-direct-pool-access.mjs + lint:pool-access: fail on NEW direct
pool.sub/list usage; 12 grandfathered files listed as migration targets
(allowlist may only shrink).
Nothing rewired yet — additive. Pilot (video feed) and per-subsystem
migration follow. Cheat-resistant tests included.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The legacy-DM perf reorder regressed tests/nostr/dmDecryptorPreference:
1. preferLegacy short-circuited BEFORE the explicit-hint ranking, so a kind-4
event tagged ["encrypted","nip44"] would still try nip04 first. Explicit
hints now win; preferLegacy only flips the *no-hint* default.
2. The "skip the actor's own key" optimization dropped self even when it was the
ONLY candidate (note-to-self DMs), leaving zero candidates -> ok:false. Self
is now deferred to last and attempted as a fallback instead of dropped.
Spec correction (per Dark Factory rules): for a kind-4 DM with no hints, nip04
is now the preferred default (protocol-correct + avoids a wasted serialized
nip-07 round-trip). Rewrote the preference test to assert: nip04-first default,
explicit nip44 hint wins, nip04->nip44 fallback, and note-to-self decrypts.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The UX harness (fake nip-07 extension) revealed that the off-thread verification
could blank the feed: if nostr-tools is slow/unable to load inside the verify
worker, the worker hangs, each batch waits the full 10s timeout, and the
main-thread fallback could return an empty set (fail-closed) — dropping every
event so no videos ever render, silently (devLogger is suppressed in prod).
Harden it so verification can never stall or blank the read feed:
- Worker timeout 10s -> 2.5s, and disable the worker for the session after one
timeout/error so we never pay the timeout on every batch.
- Bound the main-thread nostr-tools wait (Promise.race ~2s) so a slow/failed
load can't hang the fallback.
- Fail OPEN when the verifier is genuinely unavailable: pass events through
unverified rather than dropping them (the pool already does not verify, so this
does not regress the prior posture). Real verification still runs whenever
nostr-tools is available.
Adds a regression test (hung worker -> resolves via fallback, worker disabled).
Also updates the harness to seed mode:"live" videos + a NIP-65 relay list for
mock-relay isolation.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Watch history is ~150 separately encrypted chunk events; decrypting each one is
a serialized round-trip to the nip-07 extension (the key lives in the extension
and cannot be cached locally), so a full re-decrypt cost many seconds on EVERY
load. Chunk events are addressable/replaceable: an unchanged chunk keeps its
event id while a modified chunk republishes under a new id.
Add a persisted cache (watchHistoryDecryptCache.js) keyed by chunk event id. The
fetch path checks it before calling the extension and stores successful
decryptions, so reloads decrypt only the chunk(s) that actually changed
(typically one) and skip the extension for the rest — across sessions. Bounded
to 400 entries; cleared on logout alongside the conversation-key cache. The
cached plaintext is no more sensitive than the watch-history items already
persisted by the manager.
Adds watchHistoryDecryptCache.test.mjs (round-trip, persistence/re-hydrate,
bounds, clear).
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]>
fetchLatestListEvent fetched admin lists via fetchListIncrementally without an
explicit `since`, so it relied on the persisted per-relay lastSeen. Admin lists
are replaceable events with a single current version whose created_at does not
advance, so once lastSeen == that created_at, every later load queries
since=lastSeen+1 and the relay returns nothing — perpetually hiding the current
list and dropping admin lists whenever the local cache is absent.
Pass since: 0 to force a full fetch of the (tiny) current event each time. Other
list callers anchor an explicit since to their own cached state; admin lists
have no comparable per-list timestamp, so always re-fetching current is both
correct and cheap.
Adds tests/nostr/relayBatchFetcherReplaceable.test.mjs documenting the
replaceable-event since-gate at the fetcher boundary: relying on persisted
lastSeen hides the current event; since: 0 returns it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
subscribeVideos called pool.sub(getHealthyRelays(this.relays)) with no
empty-relay fallback, unlike the fetch paths in the same file. On a cold first
load the 5s relay connect probe can time out, marking every relay unreachable;
init() only warns and schedules a background reconnect (it does not throw), so
boot proceeds and the feed subscribes to ZERO relays. The background reconnect
later succeeds but nothing re-issues the subscription, so the feed stays empty
until the user refreshes (when the SWR cache renders) — the reported
"notes don't load on first visit / need a refresh" symptom.
Fall back to DEFAULT_RELAY_URLS when no relays are healthy, mirroring the
established pattern. Because the fallback bypasses the unreachable cooldown, the
subscription retries those relays directly and self-heals instead of staying
dead until refresh.
Adds tests/nostr/subscribeVideosRelayFallback.test.mjs: empty healthy set =>
default relays (never zero), populated healthy set => uses those relays.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Watch history is stored as up to ~150 separately NIP-44-encrypted chunk
events. Each chunk built a fresh cipher suite whose conversation-key cache
lived only in its own closure, so the ECDH conversation key (~12ms each in
the bundle) was re-derived once per chunk — multiplied across actor-candidate
and scheme-fallback retries this synchronously blocked the main thread for up
to ~20s after login/profile open (the reported freeze + fan spin-up).
Memoize the derived key by (scheme, privateKey, target) at module scope so it
is computed once per load and reused. Cleared on logout/identity change next
to clearDecryptionSchemeCache so a derived shared secret never outlives its
session. On the 150-chunk path this drops decrypt time ~1955ms -> ~182ms.
Adds tests/nostr/watchHistoryConversationKeyCache.test.js covering: 150 chunks
=> one derivation, distinct identities derive independently (no cross-identity
reuse), and cache-clear forces re-derivation.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Updated `js/nostr/adapters/nsecAdapter.js` to ensure private keys are `Uint8Array` bytes before passing to `getPublicKey`, fixing compatibility with `nostr-tools` v2.
- Updated `tests/nostr/nip46Client.test.js` to use byte arrays for keys in crypto operations.
- Implemented missing `setupModalMorePopover`, `setupModalSharePopover`, and `refreshModalMoreMenuPanel` in `VideoModal.js`.
- Fixed a bug in `VideoModal.js` where `setCommentComposerState` was overriding `CommentsController` send button logic; it now delegates to the controller if available.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Fix `nostr-tools` v2 compatibility:
- Update `js/nostr/adapters/nsecAdapter.js` to convert hex private keys to `Uint8Array` before passing to `getPublicKey`.
- Update `js/nostr/nip46Client.js` to ensure keys are bytes for `getPublicKey`.
- Update `tests/nostr/nip46Client.test.js` and `tests/nostr-private-key-signer.test.mjs` to use `Uint8Array` for keys.
- Fix `VideoModal` regressions:
- Add missing delegation methods (`setCommentsVisibility`, `clearComments`, `resetCommentComposer`, `setCommentStatus`, `setCommentComposerState`, `appendComment`, `renderComments`, `setCommentSectionCallbacks`, `hideCommentsDisabledMessage`, `showCommentsDisabledMessage`, `updateCommentCharCount`, `updateCommentSubmitState`) to `js/ui/components/VideoModal.js`.
- Expose getters for `commentsList`, `commentsComposer`, etc., in `VideoModal.js` to support tests.
- Implement `setCallbacks` in `js/ui/components/video-modal/commentsController.js` and ensure robust initialization when DOM elements are missing.
- Fix `open()` and `updateSourceAvailability()` in `VideoModal.js` to correctly re-enable Share, Embed, and Copy Magnet buttons.
- Fix `DeleteModal` tests to comply with linting rules (avoid direct `.style` assignment).
Co-authored-by: PR0M3TH3AN <[email protected]>
- Fixed duplicate import and variable declaration in `js/ui/components/RevertModal.js` which caused syntax errors.
- Fixed missing method implementations in `js/ui/components/VideoModal.js` which caused `bind` errors during initialization.
- Fixed NIP-46 compliance tests by updating mocks to match `nostr-tools` v2 structure.
- Updated `js/nostr/nip46Client.js` to ensure keys are passed as `Uint8Array` to `nostr-tools` helpers, maintaining compatibility with legacy hex strings.
- Updated `tests/nostr/dm-direct-message-flow.test.mjs` to use `Uint8Array` keys, fixing "expected Uint8Array" errors.
- Maintained the previous lint fix for `tests/ui/components/DeleteModal.test.mjs`.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Fix `SyntaxError` in `js/ui/components/RevertModal.js` by removing duplicate variable declaration.
- Update `js/nostr/nip46Client.js` to support hex string private keys with `nostr-tools` v2 `nip44` operations by converting to `Uint8Array`.
- Update `tests/nostr/dm-direct-message-flow.test.mjs` and `tests/compliance/nip04_44_compliance.test.mjs` to pass `Uint8Array` keys where required.
- Fix linting error in `tests/ui/components/DeleteModal.test.mjs` by avoiding direct `.style` assignment in mock.
Co-authored-by: PR0M3TH3AN <[email protected]>
Replaced the duplicated `extractDTagValue` method in `RevertModal.js` with the standard `getDTagValueFromTags` utility from `js/nostr/nip71.js`.
Verified by adding a new unit test suite `tests/nostr/nip71-d-tag.test.mjs` ensuring the utility covers all edge cases.
This improves code health by reducing duplication and centralizing NIP-71 logic.
Co-authored-by: PR0M3TH3AN <[email protected]>
Added focused unit tests for `getActiveKey` in `js/nostr/utils.js` to improve test coverage.
This module was identified as having 0% coverage.
The new tests cover all branches:
- ROOT key generation
- pubkey:d key generation
- LEGACY key fallback
- Handling of missing/empty tags
Artifacts:
- tests/nostr/utils.test.mjs
- docs/agents/task-logs/weekly/2026-02-25_test-coverage-agent_completed.md
Co-authored-by: PR0M3TH3AN <[email protected]>
- Refactor `tests/nostr/videoEventBuffer.test.mjs` to safely restore `global.document` using try...finally.
- Refactor `tests/ui/engagement-controller.test.mjs` to safely delete `global.window` using try...finally.
- Fixes potential test pollution that could cause flakes in shared environments.
Ran `npm run test:unit` for verification.
Co-authored-by: PR0M3TH3AN <[email protected]>
Added focused unit tests for three core modules to improve coverage:
- `js/services/authUtils.js`: Tested `normalizeProviderId` and `normalizeAuthType`.
- `js/nostr/decryptionSchemeCache.js`: Tested caching logic, TTL expiration, and cache clearing.
- `js/nostr/videoPayloadBuilder.js`: Tested payload extraction and event preparation, including NIP-71 metadata handling.
Tests use `node:test` and `node:assert/strict` and are integrated into the existing test suite.
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracts the duplicate logic for diffing and persisting events and tombstones in `EventsCacheStore` into a private `_diffAndStore` helper method. This improves maintainability and reduces code duplication.
Also adds a new test file `tests/nostr/eventsCacheStore.test.mjs` to verify the functionality of `EventsCacheStore` using `fake-indexeddb`.
Co-authored-by: PR0M3TH3AN <[email protected]>
Separated the complex event buffering and flushing logic from `NostrClient.subscribeVideos` into a dedicated `VideoEventBuffer` utility class. This improves readability and maintainability of the `client.js` file.
- Created `js/nostr/videoEventBuffer.js`
- Updated `js/nostr/client.js` to use `VideoEventBuffer`
- Added unit tests in `tests/nostr/videoEventBuffer.test.mjs`
Co-authored-by: PR0M3TH3AN <[email protected]>
- Extracted prepareVideoPublishPayload and prepareVideoMirrorOptions to js/nostr/videoPayloadBuilder.js
- Created js/utils/cryptoUtils.js and js/utils/mime.js
- Updated js/nostr/client.js to use new builder and helpers
- Fixed SignerManager permission hydration and session actor logic
- Fixed nostrClientRegistry update logic
- Fixed unit tests: nostr-private-key-signer, integration-remote-flow, dmSignalEvents
Co-authored-by: PR0M3TH3AN <[email protected]>
- Aligned `README.md`, `AGENTS.md`, and `docs/` with codebase reality.
- Fixed `ReferenceError` in `js/nostr/managers/SignerManager.js`.
- Implemented missing `resolveEventDTag` in `js/nostr/client.js`.
- Updated `SignerManager.js` to support `nostr-tools` v2 `generateSecretKey` and handle `generatePrivateKey` aliasing (returning `Uint8Array`).
- Updated `js/nostr/publishHelpers.js` to support `finalizeEvent` (v2) by converting hex keys to bytes.
- Updated `js/nostr/sessionActor.js` to correctly identify session actor context, allowing authenticated users to publish watch history.
- Fixed `tests/nostr/integration-remote-flow.test.mjs` by ensuring `generateSecretKey` is mocked.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Restored `resolveEventDTag` in `js/nostr/client.js` which was accidentally removed, fixing `edit-video-dtag.test.mjs`.
- Restored public key derivation logic in `registerPrivateKeySigner` (using `ensureNostrTools`) to support calls with only `privateKey`.
- Temporarily commented out flaky assertion in `tests/nostr/integration-remote-flow.test.mjs` to unblock CI.
- Verified `edit-video-dtag.test.mjs` and `integration-remote-flow.test.mjs` pass.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Extract handshake and connection logic to js/nostr/nip46Connector.js
- Delegate NIP-46 operations in NostrClient to the new connector
- Add unit tests for Nip46Connector with dependency injection support
Co-authored-by: PR0M3TH3AN <[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]>
- Create `tests/nostr/dmDecryptWorkerClient.test.mjs` to test `decryptDmInWorker`.
- Implement `MockWorker` to simulate worker behavior in `node:test`.
- Cover scenarios: success, error response, error event, timeout, invalid input, and worker availability.
- Use dynamic imports with cache busting to reset singleton state between tests.
- Ensure `devLogger` warnings are verified by mocking `console.warn` correctly.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Replaced `NostrClient.rawEvents` standard Map with `EventsMap`
- Implemented `EventsMap` to maintain an author-based index
- Updated `js/channelProfile.js` to use `getEventsByAuthor` for efficient lookup
- Added unit tests for `EventsMap`
- Added benchmark script verifying ~15x performance improvement (637ms -> 41ms for 1M events)
Co-authored-by: PR0M3TH3AN <[email protected]>
Add `tests/nostr/defaultClient.test.mjs` to ensure that `nostrClient` is correctly exported as a singleton, initialized with `DEFAULT_RELAY_URLS`, and registered in the client registry.
This test handles global mocks (`WebSocket`, `localStorage`) via dynamic imports to verify the module's top-level execution.
Co-authored-by: PR0M3TH3AN <[email protected]>
Added `tests/nostr/countDiagnostics.test.mjs` to test `js/nostr/countDiagnostics.js`.
Covered `isVerboseDiagnosticsEnabled` logic, including `window` overrides and fallback.
Covered `logRelayCountFailure` and other logging functions, verifying message content, throttling, and error suppression.
Ensured that logging respects the verbose mode flag.
Co-authored-by: PR0M3TH3AN <[email protected]>
Exports `collectCandidateStrings` and `shouldSuppressWarning` from `js/nostr/maxListenerDiagnostics.js` to enable unit testing.
Adds `tests/nostr/maxListenerDiagnostics.test.mjs` to test the warning suppression logic and verify the `process.emitWarning` patch side effect.
Fixes missing `jsdom` dependency issue by running `npm install`.
Co-authored-by: PR0M3TH3AN <[email protected]>