Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/ndk/lib/domain_layer/entities/request_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,19 @@ class RequestState {
addRequest(key, filters);
}

/// Closes the network stream if the request was not sent to any relay.
/// Engines call this once they are done dispatching: without a relay to
/// answer, nothing would end the request but its timeout. A send path that
/// is still working out its connection still counts, it may yet register one.
void closeIfNoRelays() {
if (requests.isNotEmpty ||
pendingConnections > 0 ||
networkController.isClosed) {
return;
}
networkController.close();
}

/// closes all streams
Future<void> close() async {
if (_timeout != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class JitEngine with Logger implements NetworkEngine {
final cleanedExplicitRelays = cleanRelayUrls(
requestState.request.explicitRelays!.toList(),
);
RelayJitRequestSpecificStrategy.handleRequest(
await RelayJitRequestSpecificStrategy.handleRequest(
relayManager: relayManagerLight,
requestState: requestState,
filter: filter,
Expand Down Expand Up @@ -143,6 +143,8 @@ class JitEngine with Logger implements NetworkEngine {
closeOnEOSE: ndkRequest.closeOnEOSE,
);
}

requestState.closeIfNoRelays();
}

/// broadcasts given event using inbox/outbox (gossip) if explicit relays are given they are used instead
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import '../../relay_manager.dart';
///
class RelayJitRequestSpecificStrategy {
/// send out the request
static void handleRequest({
static Future<void> handleRequest({
required RequestState requestState,
required Filter filter,
required bool closeOnEOSE,
Expand Down
4 changes: 4 additions & 0 deletions packages/ndk/lib/domain_layer/usecases/relay_sets_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ class RelaySetsEngine implements NetworkEngine {
}
_globalState.inFlightRequests[state.id] = state;

state.closeIfNoRelays();

for (MapEntry<RelayConnectionKey, RelayRequestState> entry
in state.requests.entries.toList()) {
doRelayRequest(state.id, entry.value).then((sent) {
Expand Down Expand Up @@ -282,6 +284,8 @@ class RelaySetsEngine implements NetworkEngine {
}
_globalState.inFlightRequests[state.id] = state;

state.closeIfNoRelays();

for (MapEntry<RelayConnectionKey, RelayRequestState> entry
in state.requests.entries) {
doRelayRequest(state.id, entry.value).then((sent) {
Expand Down
10 changes: 10 additions & 0 deletions packages/ndk/test/mocks/mock_relay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ class MockRelay {
/// in the middle of an authentication does. The next ones are answered
int silenceFirstAuths;

/// accept REQ messages but never answer them, neither with events nor with
/// an EOSE, the way a relay that is alive but stuck does
bool ignoreRequests;

// NIP-46 Remote Signer Support
static const int kNip46Kind = BunkerRequest.kKind;

Expand Down Expand Up @@ -187,6 +191,7 @@ class MockRelay {
this.closeRequestsMessage,
this.silenceRequests = false,
this.silenceFirstAuths = 0,
this.ignoreRequests = false,
int? explicitPort,
}) : _nip65s = nip65s,
_explicitPort = explicitPort,
Expand Down Expand Up @@ -508,6 +513,11 @@ class MockRelay {
return;
}

if (ignoreRequests) {
log("MockRelay: ignoring REQ $requestId");
return;
}

if (filters.isNotEmpty) {
// Store the subscription for this client
_clientSubscriptions[webSocket]?[requestId] = filters;
Expand Down
65 changes: 65 additions & 0 deletions packages/ndk/test/timeouts/no_relay_timeout_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:ndk/ndk.dart';
import 'package:test/test.dart';

import '../mocks/mock_event_verifier.dart';

/// a request sent to zero relays has nothing to wait for and must not hang
/// until the query timeout
void main() {
const queryTimeout = Duration(seconds: 10);
const maxAcceptable = Duration(seconds: 5);

Future<void> expectFastEmptyQuery(Ndk ndk) async {
bool timeoutTriggered = false;

final stopwatch = Stopwatch()..start();
final response = ndk.requests.query(
filter: Filter(kinds: [Nip01Event.kTextNodeKind]),
timeout: queryTimeout,
timeoutCallback: () {
timeoutTriggered = true;
},
);

final events = await response.future;
stopwatch.stop();

expect(events, isEmpty);
expect(timeoutTriggered, isFalse);
expect(stopwatch.elapsed, lessThan(maxAcceptable));
}

group('no relay - query does not wait for the timeout', () {
late Ndk ndk;

tearDown(() async {
await ndk.destroy();
});

test('relay sets engine', () async {
ndk = Ndk(
NdkConfig(
eventVerifier: MockEventVerifier(),
cache: MemCacheManager(),
engine: NdkEngine.RELAY_SETS,
bootstrapRelays: [],
),
);

await expectFastEmptyQuery(ndk);
});

test('jit engine', () async {
ndk = Ndk(
NdkConfig(
eventVerifier: MockEventVerifier(),
cache: MemCacheManager(),
engine: NdkEngine.JIT,
bootstrapRelays: [],
),
);

await expectFastEmptyQuery(ndk);
});
});
}
9 changes: 7 additions & 2 deletions packages/ndk/test/timeouts/query_timeout_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ void main() {
group('Timeout - query', () {
KeyPair key1 = Bip340.generatePrivateKey();
late MockRelay relay1;
late MockRelay silentRelay;
late Ndk ndk;
Map<KeyPair, Nip01Event> key1TextNotes = {key1: textNote(key1)};

Expand All @@ -28,11 +29,15 @@ void main() {
relay1 = MockRelay(name: "relay 1");
await relay1.startServer();
relay1.textNotes = key1TextNotes;

silentRelay = MockRelay(name: "silent relay", ignoreRequests: true);
await silentRelay.startServer();
});

tearDown(() async {
await ndk.destroy();
await relay1.stopServer();
await silentRelay.stopServer();
});

test('timeout does not trigger on normal request', () async {
Expand Down Expand Up @@ -74,7 +79,7 @@ void main() {
eventVerifier: MockEventVerifier(),
cache: MemCacheManager(),
engine: NdkEngine.RELAY_SETS,
bootstrapRelays: ["invalid"],
bootstrapRelays: [silentRelay.url],
);

ndk = Ndk(config);
Expand Down Expand Up @@ -111,7 +116,7 @@ void main() {
eventVerifier: MockEventVerifier(),
cache: MemCacheManager(),
engine: NdkEngine.RELAY_SETS,
bootstrapRelays: ["invalid"],
bootstrapRelays: [silentRelay.url],
defaultQueryTimeout: myTimeout,
);

Expand Down
Loading