Skip to content

fix: pause timeout on signing #398#399

Open
nogringo wants to merge 8 commits intomasterfrom
fix/issue-398-timeout-during-signing
Open

fix: pause timeout on signing #398#399
nogringo wants to merge 8 commits intomasterfrom
fix/issue-398-timeout-during-signing

Conversation

@nogringo
Copy link
Collaborator

@nogringo nogringo commented Jan 31, 2026

#398

Summary by CodeRabbit

  • New Features

    • Enhanced timeout control: timeouts now start explicitly and can be paused/resumed during authentication flows
    • Improved reliability for operations with slower signing methods
  • Tests

    • Added integration tests validating successful operations with slow signers under timeout constraints
    • Expanded test infrastructure with new mock slow signer implementation

@frnandu
Copy link
Collaborator

frnandu commented Feb 3, 2026

Wouldn't we need 2 timeouts then? Because the app logic might need to also have a time limit for signing, if the signer is unresponsive.

@nogringo
Copy link
Collaborator Author

nogringo commented Feb 4, 2026

Related to #236

@1-leo
Copy link
Contributor

1-leo commented Feb 4, 2026

grafik

For not implement solution 2

tbd: e2e timeout and what to make the default value ajusted by the dev (what to communicate)
e2e || excluding timer signeout (e.g. cache+network)

In cause of signer required after network query its included in the network timeout

@nogringo nogringo force-pushed the fix/issue-398-timeout-during-signing branch from 7cc7fdb to f1817bf Compare February 18, 2026 12:33
@nogringo nogringo self-assigned this Feb 18, 2026
@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 92.68293% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.37%. Comparing base (b9db040) to head (5ae8db7).
⚠️ Report is 15 commits behind head on master.

Files with missing lines Patch % Lines
...s/ndk/lib/domain_layer/usecases/relay_manager.dart 85.71% 2 Missing ⚠️
...s/ndk/lib/domain_layer/entities/request_state.dart 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #399      +/-   ##
==========================================
+ Coverage   73.36%   73.37%   +0.01%     
==========================================
  Files         195      195              
  Lines        8969     9007      +38     
==========================================
+ Hits         6580     6609      +29     
- Misses       2389     2398       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nogringo nogringo requested review from 1-leo and frnandu February 19, 2026 09:38
@1-leo 1-leo changed the title test: add failing tests for slow signer timeout issue #398 fix: pause timeout on signing #398 Mar 11, 2026
@nogringo nogringo force-pushed the fix/issue-398-timeout-during-signing branch from ea76bc4 to 5ae8db7 Compare March 25, 2026 17:36
@coderabbitai
Copy link

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

The changes implement a deferred timeout initialization mechanism for broadcast events and pause/resume functionality for request timeouts. A new public startTimeout() method in BroadcastState allows explicit timeout triggering after signing completes. RequestState gains pause/resume capabilities to temporarily halt timeouts during asynchronous signer operations. Use case engines call the new timeout method, and RelayManager coordinates pausing and resuming timeouts across all in-flight requests during NIP-42 AUTH signing.

Changes

Cohort / File(s) Summary
Timeout State Management
packages/ndk/lib/domain_layer/entities/broadcast_state.dart, packages/ndk/lib/domain_layer/entities/request_state.dart
Added deferred timeout initialization in BroadcastState via new public startTimeout() method. Introduced pause/resume timeout capabilities in RequestState with internal helpers _startTimeout(), pauseTimeout(), and resumeTimeout() to support tracking remaining duration during pauses.
Use Case Engine Integration
packages/ndk/lib/domain_layer/usecases/jit_engine.dart, packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart
Both engines now invoke broadcastState.startTimeout() after signer work completes. Changed broadcastDoneStream construction from precomputed variable to directly mapped stream expression broadcastState.stateUpdates.map((state) => state.broadcasts.values.toList()).
Relay Manager Timeout Coordination
packages/ndk/lib/domain_layer/usecases/relay_manager.dart
Added coordinated pause/resume of per-request timeouts during NIP-42 AUTH signing. Collects request states for target relay, pauses before signature generation, and resumes after signing callbacks complete or if no accounts can sign.
Mock Utilities
packages/ndk/test/mocks/mock_relay.dart, packages/ndk/test/mocks/mock_slow_signer.dart
MockRelay now generates authentication challenge once per server lifetime instead of per-connection. New MockSlowSigner class wraps EventSigner and applies configurable Duration delay before delegating signing operations while forwarding accessors and lifecycle methods directly.
Tests
packages/ndk/test/usecases/broadcast_test.dart, packages/ndk/test/usecases/slow_signer_timeout_test.dart
Updated mock relay port assignments in broadcast test. Added new integration test file validating NDK broadcast and query operations complete successfully when signers introduce delays exceeding configured timeout values.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Engine as JIT/RelaySet Engine
    participant BroadcastState
    participant RelayManager
    participant Signer as MockSlowSigner
    participant RequestState

    Client->>Engine: broadcast event / query
    Engine->>Signer: sign event (async with delay)
    
    Note over RelayManager: During slow signing...
    Engine->>RelayManager: pause timeouts for in-flight requests
    RelayManager->>RequestState: pauseTimeout()
    RequestState->>RequestState: store remaining duration
    
    Signer->>Signer: apply 12s delay
    Signer-->>Engine: return signed event
    
    Engine->>RelayManager: resume timeouts
    RelayManager->>RequestState: resumeTimeout()
    RequestState->>RequestState: restart with remaining duration
    
    Engine->>BroadcastState: startTimeout()
    BroadcastState->>BroadcastState: initialize timeout timer
    
    Engine->>Engine: dispatch to relays
    Engine-->>Client: broadcast/query result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 The timeout hops in at just the right time,
No more rushing when signers are slow to unwind,
Pause, breathe, resume—a graceful dance,
Broadcast and queries get their proper chance!
⏱️✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: pause timeout on signing #398' clearly and specifically describes the main change: pausing request timeouts during the signing phase to handle slow signers. It directly relates to the primary objective of the PR and is concise and actionable.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-398-timeout-during-signing

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ndk/lib/domain_layer/usecases/jit_engine/jit_engine.dart (1)

168-169: ⚠️ Potential issue | 🟡 Minor

Dead code: doneStream variable is unused.

The doneStream variable is declared but never used since lines 228-229 inline the same stream expression directly.

🧹 Remove unused variable
-    final doneStream = broadcastState.stateUpdates
-        .map((state) => state.broadcasts.values.toList());
-
     Future<void> asyncStuff() async {
🤖 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/jit_engine.dart` around
lines 168 - 169, The local variable doneStream is dead code—it's declared as
`doneStream = broadcastState.stateUpdates.map((state) =>
state.broadcasts.values.toList())` but never used; remove the `doneStream`
declaration and any associated unused imports so the code uses the inlined
stream expression directly (as already done later) and keep only the live
reference to `broadcastState.stateUpdates.map(...)`.
🧹 Nitpick comments (2)
packages/ndk/lib/domain_layer/entities/request_state.dart (1)

91-117: Well-structured pause/resume implementation.

The timeout pause/resume logic correctly handles the common cases. The guards ensure safe operation when methods are called multiple times or out of order.

One minor observation: after the timeout fires naturally, _timeout isn't nulled, so a subsequent pauseTimeout() call would attempt to calculate remaining time. In practice this is unlikely since close() ends the request lifecycle, but for defensive clarity:

🛡️ Optional: Null the timer reference after firing
  void _startTimeout(Duration duration) {
    _timeoutStartedAt = DateTime.now();
    _timeout = Timer(duration, () {
+     _timeout = null;
      onTimeout?.call(this);
      close();
    });
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ndk/lib/domain_layer/entities/request_state.dart` around lines 91 -
117, The Timer callback in _startTimeout should clear the internal timer
reference so subsequent calls like pauseTimeout() won't act on a fired timer;
modify the closure inside _startTimeout (the Timer created and assigned to
_timeout) to set _timeout = null (and optionally _timeoutStartedAt = null) after
calling onTimeout?.call(this) and close(), ensuring the _timeout field reflects
that the timer has completed.
packages/ndk/lib/domain_layer/usecases/relay_manager.dart (1)

597-633: Timeout coordination during eager AUTH looks correct but has subtle asymmetry.

The pause/resume pattern correctly prevents timeout during signing. However, with multiple accounts, the first account to complete signing resumes all timeouts while other accounts may still be signing. This differs from _handleClosedAuthRequired which waits for all signings.

For eager AUTH this is likely acceptable since requests aren't blocked on authentication, but consider aligning the patterns for consistency:

♻️ Optional: Wait for all signings before resuming (consistent with lazy AUTH)
  void _authenticateAccounts(
    RelayConnectivity relayConnectivity,
    String challenge,
    Set<Account> accounts,
  ) {
    // Pause timeout for all requests on this relay during AUTH signing
    final requestsOnRelay = globalState.inFlightRequests.values
        .where((state) => state.requests.keys.contains(relayConnectivity.url))
        .toList();
    for (final state in requestsOnRelay) {
      state.pauseTimeout();
    }

    int authCount = 0;
+   int pendingSignCount = accounts.where((a) => a.signer.canSign()).length;
+
    for (final account in accounts) {
      if (account.signer.canSign()) {
        final auth = AuthEvent(pubKey: account.pubkey, tags: [
          ["relay", relayConnectivity.url],
          ["challenge", challenge]
        ]);
        account.signer.sign(auth).then((signedAuth) {
-         // Resume timeout for requests after signing completes
-         for (final state in requestsOnRelay) {
-           state.resumeTimeout();
-         }
+         pendingSignCount--;
+         if (pendingSignCount == 0) {
+           // Resume timeout for requests after all signings complete
+           for (final state in requestsOnRelay) {
+             state.resumeTimeout();
+           }
+         }
          send(relayConnectivity,
              ClientMsg(ClientMsgType.kAuth, event: signedAuth));
🤖 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_manager.dart` around lines 597 -
633, The current eager AUTH flow pauses timeouts for requestsOnRelay but resumes
them as soon as any account.signer.sign completes, which can resume timeouts
while other accounts are still signing; change the logic in the relay auth block
so you collect the Futures returned by account.signer.sign for each
signing-capable account (or otherwise count completions) and only call
state.resumeTimeout() for all states after all signings complete (e.g., await
Future.wait on the list of sign futures), while preserving the existing
authCount==0 fast-path and the log messages; look for references to
requestsOnRelay, account.signer.sign, authCount, and resumeTimeout to update.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/ndk/lib/domain_layer/usecases/jit_engine/jit_engine.dart`:
- Around line 168-169: The local variable doneStream is dead code—it's declared
as `doneStream = broadcastState.stateUpdates.map((state) =>
state.broadcasts.values.toList())` but never used; remove the `doneStream`
declaration and any associated unused imports so the code uses the inlined
stream expression directly (as already done later) and keep only the live
reference to `broadcastState.stateUpdates.map(...)`.

---

Nitpick comments:
In `@packages/ndk/lib/domain_layer/entities/request_state.dart`:
- Around line 91-117: The Timer callback in _startTimeout should clear the
internal timer reference so subsequent calls like pauseTimeout() won't act on a
fired timer; modify the closure inside _startTimeout (the Timer created and
assigned to _timeout) to set _timeout = null (and optionally _timeoutStartedAt =
null) after calling onTimeout?.call(this) and close(), ensuring the _timeout
field reflects that the timer has completed.

In `@packages/ndk/lib/domain_layer/usecases/relay_manager.dart`:
- Around line 597-633: The current eager AUTH flow pauses timeouts for
requestsOnRelay but resumes them as soon as any account.signer.sign completes,
which can resume timeouts while other accounts are still signing; change the
logic in the relay auth block so you collect the Futures returned by
account.signer.sign for each signing-capable account (or otherwise count
completions) and only call state.resumeTimeout() for all states after all
signings complete (e.g., await Future.wait on the list of sign futures), while
preserving the existing authCount==0 fast-path and the log messages; look for
references to requestsOnRelay, account.signer.sign, authCount, and resumeTimeout
to update.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a128813c-6fe0-494d-8dfe-325142e2126a

📥 Commits

Reviewing files that changed from the base of the PR and between 7f20376 and 5ae8db7.

📒 Files selected for processing (9)
  • packages/ndk/lib/domain_layer/entities/broadcast_state.dart
  • packages/ndk/lib/domain_layer/entities/request_state.dart
  • packages/ndk/lib/domain_layer/usecases/jit_engine/jit_engine.dart
  • packages/ndk/lib/domain_layer/usecases/relay_manager.dart
  • packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart
  • packages/ndk/test/mocks/mock_relay.dart
  • packages/ndk/test/mocks/mock_slow_signer.dart
  • packages/ndk/test/usecases/broadcast_test.dart
  • packages/ndk/test/usecases/slow_signer_timeout_test.dart

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants