Conversation
📝 WalkthroughWalkthroughAwait and parallelize relay broadcasts across JIT and relay-set flows, improve relay transport null-safety and broadcast completion detection (including no-relay case), add closeIfNoRelays(), and introduce two timing tests for offline or empty-relay broadcast scenarios. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant JIT as JitEngine
participant Sets as RelaySetsEngine
participant Manager as RelayManager
participant Relay as RelayTransport
participant Network as NetworkController
Client->>JIT: request broadcast(event, strategy)
JIT->>Sets: handleEventBroadcast(event, strategy)
Sets->>Manager: select/connect relays (parallel)
Manager->>Relay: connect/reconnect
Relay-->>Manager: connected / failed
Manager-->>Sets: connection results
Sets->>Relay: sendToUrl (concurrent sends)
Relay-->>Network: publish result
Network-->>Sets: publish responses / onDone
Sets->>JIT: broadcast completion (calls closeIfNoRelays() if needed)
JIT->>Client: return broadcast outcome
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/ndk/test/broadcast/broadcast_test.dart (1)
17-40: Consider extracting shared setup/assert flow into a helper.Both tests repeat the same key generation, login, event creation, timing, and threshold assertion. A helper would reduce duplication and make future timeout-case additions easier.
Also applies to: 51-77
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ndk/test/broadcast/broadcast_test.dart` around lines 17 - 40, Tests duplicate key generation, login, event creation, timing and timeout assertion; extract that shared flow into a helper function (e.g., runBroadcastAndAssertWithin) that performs Bip340.generatePrivateKey(), calls ndk.accounts.loginPrivateKey(privkey:..., pubkey:...), constructs the Nip01Event(pubKey:..., kind:1, content:'', tags:[]), starts a Stopwatch, invokes ndk.broadcast.broadcast(nostrEvent: event), awaits broadcast.broadcastDoneFuture, stops the stopwatch and asserts stopwatch.elapsedMilliseconds is lessThan a given threshold; replace the repeated blocks in both test locations with calls to this helper and pass the threshold as a parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ndk/test/broadcast/broadcast_test.dart`:
- Around line 6-43: The test currently calls await ndk.destroy() only on the
happy path; wrap the test body so Ndk cleanup always runs by ensuring
ndk.destroy() is invoked in a finally block (or use test framework tearDown)
after creating the Ndk instance, so any exceptions from
broadcast.broadcast/broadcastDoneFuture or failed assertions still call
ndk.destroy(); locate the Ndk instantiation (Ndk(...)), the broadcast invocation
(ndk.broadcast.broadcast and broadcastDoneFuture) and ensure destroy() is called
in finally/tearDown.
---
Nitpick comments:
In `@packages/ndk/test/broadcast/broadcast_test.dart`:
- Around line 17-40: Tests duplicate key generation, login, event creation,
timing and timeout assertion; extract that shared flow into a helper function
(e.g., runBroadcastAndAssertWithin) that performs Bip340.generatePrivateKey(),
calls ndk.accounts.loginPrivateKey(privkey:..., pubkey:...), constructs the
Nip01Event(pubKey:..., kind:1, content:'', tags:[]), starts a Stopwatch, invokes
ndk.broadcast.broadcast(nostrEvent: event), awaits
broadcast.broadcastDoneFuture, stops the stopwatch and asserts
stopwatch.elapsedMilliseconds is lessThan a given threshold; replace the
repeated blocks in both test locations with calls to this helper and pass the
threshold as a parameter.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5faa1b96-231f-4dbc-8479-47488ca37fcf
📒 Files selected for processing (1)
packages/ndk/test/broadcast/broadcast_test.dart
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_other_read.dart`:
- Around line 91-94: The code uses
relayManager.connectedRelays.firstWhere((element) => element.url == relayUrl)
which can throw a StateError if no relay matches; wrap that lookup in a
try-catch (or use firstWhere with orElse) to handle the missing-relay case, and
only call sendToRelay(relay: relay) when a relay is found; ensure the catch logs
or handles the error similarly to relay_jit_broadcast_specific.dart so behavior
is consistent with other strategies.
In
`@packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_own.dart`:
- Around line 82-85: The relay lookup using
relayManager.connectedRelays.firstWhere (the block that finds a relay by
relayUrl before calling sendToRelay) can throw StateError if no match is found;
wrap that lookup in a try-catch (matching the pattern used in
relay_jit_broadcast_specific.dart) so if the relay isn't present after
connectRelay we catch the error, skip/continue the loop and do not call
sendToRelay, and optionally log the missing-relay case; update the code around
firstWhere and sendToRelay to handle the exception gracefully.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 35785250-d0da-418e-8cdd-e5cdf17196ec
📒 Files selected for processing (7)
packages/ndk/lib/domain_layer/entities/broadcast_state.dartpackages/ndk/lib/domain_layer/usecases/jit_engine/jit_engine.dartpackages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_other_read.dartpackages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_own.dartpackages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_specific.dartpackages/ndk/lib/domain_layer/usecases/relay_manager.dartpackages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #494 +/- ##
==========================================
+ Coverage 76.74% 76.80% +0.06%
==========================================
Files 153 153
Lines 6407 6424 +17
==========================================
+ Hits 4917 4934 +17
Misses 1490 1490 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_other_read.dart (1)
14-18: Consider aligningrelay_jit_broadcast_specific.dartwith this parallel pattern.The
RelayJitBroadcastSpecificRelaysStrategyinrelay_jit_broadcast_specific.dart(lines 28-69) still uses a sequentialforloop withawaitinside, while this file andrelay_jit_broadcast_own.dartnow use parallelFuture.wait. For consistent broadcast behavior and latency characteristics across all strategies, consider applying the same parallel pattern there in a follow-up.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_other_read.dart` around lines 14 - 18, The broadcast in RelayJitBroadcastSpecificRelaysStrategy currently iterates relays with a sequential for loop that awaits each send; change it to run sends in parallel using Future.wait to match RelayJitBroadcastOtherReadStrategy and RelayJitBroadcastOwnStrategy. Locate the static broadcast method in RelayJitBroadcastSpecificRelaysStrategy and replace the sequential await-per-relay logic with collecting Futures for each relay send (and any onMessage/connect futures) and await them together via Future.wait, preserving error handling and any per-relay callbacks.packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart (1)
332-351: Deduplicate inbox relay URLs before broadcasting.
myWriteRelayUrlsOtherscan contain duplicates across multiplepTags, causing redundant publishes to the same relay.Proposed refactor
- List<String> myWriteRelayUrlsOthers = []; + final Set<String> myWriteRelayUrlsOthers = <String>{}; @@ - myWriteRelayUrlsOthers.addAll(maxList); + myWriteRelayUrlsOthers.addAll(maxList); @@ - await Future.wait(myWriteRelayUrlsOthers.map((relayUrl) => + await Future.wait(myWriteRelayUrlsOthers.map((relayUrl) => doRelayBroadcast(relayUrl, workingEvent)));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart` around lines 332 - 351, The list myWriteRelayUrlsOthers may contain duplicate relay URLs gathered from iterating nip65Data which causes redundant publishes via doRelayBroadcast; change the collection/iteration to deduplicate before broadcasting (e.g., use a Set or LinkedHashSet to collect unique relay URLs after building maxList or call .toSet() / LinkedHashSet.from(myWriteRelayUrlsOthers) and then await Future.wait over the unique relay URLs) while still respecting BroadcastDefaults.MAX_INBOX_RELAYS_TO_BROADCAST and the existing logic that builds completeList/maxList.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart`:
- Around line 315-318: The else branch logs that bootstrap relays will be used
but never actually replaces writeRelaysUrls (currently sourced from
_relayManager.globalState.relays.keys), so outbox broadcasting can be skipped;
update that branch in relay_sets_engine.dart to set writeRelaysUrls to the
module's known default bootstrap list (replace the writeRelaysUrls value taken
from _relayManager.globalState.relays.keys with the default bootstrap relays
constant or property used in your codebase—e.g., defaultBootstrapRelays or
_relayManager.bootstrapRelays) immediately after the Logger.log.w call so the
subsequent broadcast uses the fallback relays.
---
Nitpick comments:
In
`@packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_other_read.dart`:
- Around line 14-18: The broadcast in RelayJitBroadcastSpecificRelaysStrategy
currently iterates relays with a sequential for loop that awaits each send;
change it to run sends in parallel using Future.wait to match
RelayJitBroadcastOtherReadStrategy and RelayJitBroadcastOwnStrategy. Locate the
static broadcast method in RelayJitBroadcastSpecificRelaysStrategy and replace
the sequential await-per-relay logic with collecting Futures for each relay send
(and any onMessage/connect futures) and await them together via Future.wait,
preserving error handling and any per-relay callbacks.
In `@packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart`:
- Around line 332-351: The list myWriteRelayUrlsOthers may contain duplicate
relay URLs gathered from iterating nip65Data which causes redundant publishes
via doRelayBroadcast; change the collection/iteration to deduplicate before
broadcasting (e.g., use a Set or LinkedHashSet to collect unique relay URLs
after building maxList or call .toSet() /
LinkedHashSet.from(myWriteRelayUrlsOthers) and then await Future.wait over the
unique relay URLs) while still respecting
BroadcastDefaults.MAX_INBOX_RELAYS_TO_BROADCAST and the existing logic that
builds completeList/maxList.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d0329a9c-eae0-419f-bb90-5323e0fe9ebf
📒 Files selected for processing (6)
packages/ndk/lib/domain_layer/entities/broadcast_state.dartpackages/ndk/lib/domain_layer/usecases/jit_engine/jit_engine.dartpackages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_other_read.dartpackages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_own.dartpackages/ndk/lib/domain_layer/usecases/relay_sets_engine.dartpackages/ndk/test/broadcast/broadcast_test.dart
✅ Files skipped from review due to trivial changes (1)
- packages/ndk/test/broadcast/broadcast_test.dart
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/ndk/lib/domain_layer/usecases/jit_engine/jit_engine.dart
- packages/ndk/lib/domain_layer/entities/broadcast_state.dart
| } else { | ||
| Logger.log.w(() => | ||
| "could not find user relay list from nip65, using default bootstrap relays"); | ||
| } |
There was a problem hiding this comment.
Fallback branch says “bootstrap relays” but does not actually set them.
At Line 316, the warning says bootstrap relays are used, but writeRelaysUrls is still sourced from Line 308 (_relayManager.globalState.relays.keys). If that list is empty/stale, outbox broadcast is skipped unexpectedly.
Proposed fix
if (nip65List.isNotEmpty) {
writeRelaysUrls = nip65List.first.relays.entries
.where((element) => element.value.isWrite)
.map((e) => e.key)
.toList();
} else {
Logger.log.w(() =>
"could not find user relay list from nip65, using default bootstrap relays");
+ writeRelaysUrls = cleanRelayUrls(_bootstrapRelays.toList());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else { | |
| Logger.log.w(() => | |
| "could not find user relay list from nip65, using default bootstrap relays"); | |
| } | |
| } else { | |
| Logger.log.w(() => | |
| "could not find user relay list from nip65, using default bootstrap relays"); | |
| writeRelaysUrls = cleanRelayUrls(_bootstrapRelays.toList()); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart` around lines
315 - 318, The else branch logs that bootstrap relays will be used but never
actually replaces writeRelaysUrls (currently sourced from
_relayManager.globalState.relays.keys), so outbox broadcasting can be skipped;
update that branch in relay_sets_engine.dart to set writeRelaysUrls to the
module's known default bootstrap list (replace the writeRelaysUrls value taken
from _relayManager.globalState.relays.keys with the default bootstrap relays
constant or property used in your codebase—e.g., defaultBootstrapRelays or
_relayManager.bootstrapRelays) immediately after the Logger.log.w call so the
subsequent broadcast uses the fallback relays.
frnandu
left a comment
There was a problem hiding this comment.
The RelayJitBroadcastSpecificRelaysStrategy in relay_jit_broadcast_specific.dart (lines 28-69) still uses a sequential for loop with await inside, while this file and relay_jit_broadcast_own.dart now use parallel Future.wait. For consistent broadcast behavior and latency characteristics across all strategies, consider applying the same parallel pattern there in a follow-up.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_specific.dart`:
- Around line 43-54: The catch currently treats any error (including send
failures) as "relay not found" in the block using connectedRelays.firstWhere and
sendToRelay; change the flow in relay_jit_broadcast_specific.dart so you first
determine existence of the relay without wrapping send in the same try/catch
(e.g., use firstWhere with orElse or a simple connectedRelays.any/check to see
if a relay with url == relayUrl exists), if not found call
relayManager.failBroadcast(eventToPublish.id, relayUrl, "relay not found in
connected list"), otherwise call sendToRelay(relay: relay) inside its own
try/catch and on send errors call relayManager.failBroadcast(eventToPublish.id,
relayUrl, "send failed: <error message>"); apply the same separation of lookup
vs send error handling to the similar block around lines 70-80.
- Around line 41-47: The connectivity check uses
relayManager.isRelayConnected(relayUrl) but the relay instance is retrieved from
a possibly stale connectedRelays snapshot (connectedRelays.firstWhere), causing
false failures; after confirming isRelayConnected, fetch the relay from the same
source (relayManager) instead of connectedRelays — e.g., use a relayManager
method to get the connected relay by relayUrl (or add one if missing) and then
call sendToRelay(relay: ...) so both connectivity and the relay instance come
from relayManager (symbols: relayManager.isRelayConnected, connectedRelays,
sendToRelay, relayUrl).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c23c1b33-3034-4836-beec-9ebcb8be7d5b
📒 Files selected for processing (1)
packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_specific.dart
| final isConnected = relayManager.isRelayConnected(relayUrl); | ||
| if (isConnected) { | ||
| try { | ||
| final relay = connectedRelays.firstWhere( | ||
| (element) => element.url == relayUrl, | ||
| ); | ||
| sendToRelay(relay: relay); |
There was a problem hiding this comment.
Use one relay source after connectivity check to avoid false failures.
Line 41 checks connectivity from relayManager, but Line 44 looks up from the connectedRelays snapshot. If that snapshot is stale, a connected relay can be incorrectly marked failed.
Suggested fix
- final relay = connectedRelays.firstWhere(
+ final relay = relayManager.connectedRelays.firstWhere(
(element) => element.url == relayUrl,
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_specific.dart`
around lines 41 - 47, The connectivity check uses
relayManager.isRelayConnected(relayUrl) but the relay instance is retrieved from
a possibly stale connectedRelays snapshot (connectedRelays.firstWhere), causing
false failures; after confirming isRelayConnected, fetch the relay from the same
source (relayManager) instead of connectedRelays — e.g., use a relayManager
method to get the connected relay by relayUrl (or add one if missing) and then
call sendToRelay(relay: ...) so both connectivity and the relay instance come
from relayManager (symbols: relayManager.isRelayConnected, connectedRelays,
sendToRelay, relayUrl).
| try { | ||
| final relay = connectedRelays.firstWhere( | ||
| (element) => element.url == relayUrl, | ||
| ); | ||
| sendToRelay(relay: relay); | ||
| } catch (e) { | ||
| relayManager.failBroadcast( | ||
| eventToPublish.id, | ||
| relayUrl, | ||
| "relay not found in connected list", | ||
| ); | ||
| } |
There was a problem hiding this comment.
Narrow catch scope: current error mapping can misclassify send failures.
The catch blocks label all exceptions as “relay not found”, but they also wrap sendToRelay(...). A send exception will be reported with the wrong reason.
Suggested fix pattern
- try {
- final relay = relayManager.connectedRelays
- .firstWhere((element) => element.url == relayUrl);
- sendToRelay(relay: relay);
- } catch (e) {
- relayManager.failBroadcast(
- eventToPublish.id,
- relayUrl,
- "relay not found after connection",
- );
- }
+ final matches = relayManager.connectedRelays
+ .where((element) => element.url == relayUrl);
+ if (matches.isEmpty) {
+ relayManager.failBroadcast(
+ eventToPublish.id,
+ relayUrl,
+ "relay not found after connection",
+ );
+ return;
+ }
+ try {
+ sendToRelay(relay: matches.first);
+ } catch (e) {
+ relayManager.failBroadcast(
+ eventToPublish.id,
+ relayUrl,
+ "send error: $e",
+ );
+ }Also applies to: 70-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/ndk/lib/domain_layer/usecases/jit_engine/relay_jit_broadcast_strategies/relay_jit_broadcast_specific.dart`
around lines 43 - 54, The catch currently treats any error (including send
failures) as "relay not found" in the block using connectedRelays.firstWhere and
sendToRelay; change the flow in relay_jit_broadcast_specific.dart so you first
determine existence of the relay without wrapping send in the same try/catch
(e.g., use firstWhere with orElse or a simple connectedRelays.any/check to see
if a relay with url == relayUrl exists), if not found call
relayManager.failBroadcast(eventToPublish.id, relayUrl, "relay not found in
connected list"), otherwise call sendToRelay(relay: relay) inside its own
try/catch and on send errors call relayManager.failBroadcast(eventToPublish.id,
relayUrl, "send failed: <error message>"); apply the same separation of lookup
vs send error handling to the similar block around lines 70-80.
Summary by CodeRabbit