From 3b124fcb1d5b904eb9c002a79b23017ceed289e6 Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 7 Jan 2026 13:55:38 +0100 Subject: [PATCH 01/13] feat: coverage usecase --- .../cache_manager/mem_cache_manager.dart | 63 ++++ .../entities/filter_coverage.dart | 275 ++++++++++++++++++ .../repositories/cache_manager.dart | 32 ++ .../usecases/coverage/coverage.dart | 272 +++++++++++++++++ packages/ndk/lib/ndk.dart | 2 + packages/ndk/lib/presentation_layer/init.dart | 6 + packages/ndk/lib/presentation_layer/ndk.dart | 5 + 7 files changed, 655 insertions(+) create mode 100644 packages/ndk/lib/domain_layer/entities/filter_coverage.dart create mode 100644 packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart diff --git a/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart b/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart index 0781e7c3e..e2de7ad3b 100644 --- a/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart +++ b/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart @@ -1,6 +1,7 @@ import 'dart:core'; import '../../../domain_layer/entities/contact_list.dart'; +import '../../../domain_layer/entities/filter_coverage.dart'; import '../../../domain_layer/entities/nip_01_event.dart'; import '../../../domain_layer/entities/nip_05.dart'; import '../../../domain_layer/entities/relay_set.dart'; @@ -30,6 +31,10 @@ class MemCacheManager implements CacheManager { /// In memory storage Map events = {}; + /// In memory storage for filter coverage records + /// Key is filterHash:relayUrl:rangeStart + Map filterCoverageRecords = {}; + @override Future saveUserRelayList(UserRelayList userRelayList) async { userRelayLists[userRelayList.pubKey] = userRelayList; @@ -297,4 +302,62 @@ class MemCacheManager implements CacheManager { Future close() async { return; } + + // ===================== + // Filter Coverage + // ===================== + + @override + Future saveFilterCoverageRecord(FilterCoverageRecord record) async { + filterCoverageRecords[record.key] = record; + } + + @override + Future saveFilterCoverageRecords( + List records) async { + for (final record in records) { + filterCoverageRecords[record.key] = record; + } + } + + @override + Future> loadFilterCoverageRecords( + String filterHash) async { + return filterCoverageRecords.values + .where((r) => r.filterHash == filterHash) + .toList(); + } + + @override + Future> loadFilterCoverageRecordsByRelay( + String filterHash, String relayUrl) async { + return filterCoverageRecords.values + .where((r) => r.filterHash == filterHash && r.relayUrl == relayUrl) + .toList(); + } + + @override + Future> loadFilterCoverageRecordsByRelayUrl( + String relayUrl) async { + return filterCoverageRecords.values + .where((r) => r.relayUrl == relayUrl) + .toList(); + } + + @override + Future removeFilterCoverageRecords(String filterHash) async { + filterCoverageRecords + .removeWhere((key, value) => value.filterHash == filterHash); + } + + @override + Future removeFilterCoverageRecordsByRelay(String relayUrl) async { + filterCoverageRecords + .removeWhere((key, value) => value.relayUrl == relayUrl); + } + + @override + Future removeAllFilterCoverageRecords() async { + filterCoverageRecords.clear(); + } } diff --git a/packages/ndk/lib/domain_layer/entities/filter_coverage.dart b/packages/ndk/lib/domain_layer/entities/filter_coverage.dart new file mode 100644 index 000000000..33329a74d --- /dev/null +++ b/packages/ndk/lib/domain_layer/entities/filter_coverage.dart @@ -0,0 +1,275 @@ +import 'dart:convert'; +import 'dart:math'; +import 'package:crypto/crypto.dart'; + +import 'filter.dart'; + +/// A time range that has been fetched from a relay +class TimeRange { + /// Start timestamp (inclusive) + final int since; + + /// End timestamp (inclusive) + final int until; + + /// When this range was fetched + final int fetchedAt; + + const TimeRange({ + required this.since, + required this.until, + required this.fetchedAt, + }); + + /// Check if this range overlaps or is adjacent to another range + bool canMergeWith(TimeRange other) { + // Adjacent or overlapping: one's end touches or exceeds the other's start + return until >= other.since - 1 && since <= other.until + 1; + } + + /// Merge this range with another (assumes they can be merged) + TimeRange mergeWith(TimeRange other) { + return TimeRange( + since: min(since, other.since), + until: max(until, other.until), + fetchedAt: max(fetchedAt, other.fetchedAt), + ); + } + + /// Check if this range fully contains a time period + bool contains(int start, int end) { + return since <= start && until >= end; + } + + /// Check if this range overlaps with a time period + bool overlaps(int start, int end) { + return since <= end && until >= start; + } + + @override + String toString() => 'TimeRange($since - $until, fetchedAt: $fetchedAt)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TimeRange && + since == other.since && + until == other.until && + fetchedAt == other.fetchedAt; + + @override + int get hashCode => since.hashCode ^ until.hashCode ^ fetchedAt.hashCode; + + Map toJson() => { + 'since': since, + 'until': until, + 'fetchedAt': fetchedAt, + }; + + factory TimeRange.fromJson(Map json) => TimeRange( + since: json['since'] as int, + until: json['until'] as int, + fetchedAt: json['fetchedAt'] as int, + ); +} + +/// A gap in coverage (missing time range) +class CoverageGap { + final String relayUrl; + final int since; + final int until; + + const CoverageGap({ + required this.relayUrl, + required this.since, + required this.until, + }); + + @override + String toString() => 'CoverageGap($relayUrl: $since - $until)'; +} + +/// Coverage information for a specific relay and filter combination +class RelayCoverage { + final String relayUrl; + final Filter filter; + final List ranges; + final int? reachedOldestAt; + + const RelayCoverage({ + required this.relayUrl, + required this.filter, + required this.ranges, + this.reachedOldestAt, + }); + + /// Whether we've confirmed there's no older data on this relay + bool get reachedOldest => reachedOldestAt != null; + + /// Oldest timestamp we have data for (start of first range) + int? get oldest { + if (ranges.isEmpty) return null; + return ranges.map((r) => r.since).reduce(min); + } + + /// Newest timestamp we have data for (end of last range) + int? get newest { + if (ranges.isEmpty) return null; + return ranges.map((r) => r.until).reduce(max); + } + + /// Calculate gaps between ranges for a given time period + List findGaps(int since, int until) { + if (ranges.isEmpty) { + return [TimeRange(since: since, until: until, fetchedAt: 0)]; + } + + // Sort ranges by since + final sortedRanges = List.from(ranges) + ..sort((a, b) => a.since.compareTo(b.since)); + + final gaps = []; + var currentPos = since; + + for (final range in sortedRanges) { + // Skip ranges that end before our period starts + if (range.until < since) continue; + // Stop if range starts after our period ends + if (range.since > until) break; + + // Gap before this range? + if (currentPos < range.since) { + gaps.add(TimeRange( + since: currentPos, + until: min(range.since - 1, until), + fetchedAt: 0, + )); + } + + // Move position past this range + currentPos = max(currentPos, range.until + 1); + } + + // Gap after last range? + if (currentPos <= until) { + gaps.add(TimeRange( + since: currentPos, + until: until, + fetchedAt: 0, + )); + } + + return gaps; + } + + /// Get gaps as CoverageGap objects + List getGaps(int since, int until) { + return findGaps(since, until) + .map((g) => CoverageGap( + relayUrl: relayUrl, + since: g.since, + until: g.until, + )) + .toList(); + } + + @override + String toString() => + 'RelayCoverage($relayUrl, ${ranges.length} ranges, oldest: $oldest, newest: $newest, reachedOldest: $reachedOldest)'; +} + +/// Record stored in the database for filter coverage +class FilterCoverageRecord { + /// Hash of the filter (without since/until) + final String filterHash; + + /// The relay URL + final String relayUrl; + + /// Start of the covered range + final int rangeStart; + + /// End of the covered range + final int rangeEnd; + + /// When this range was fetched + final int fetchedAt; + + /// Whether we've reached the oldest data on this relay for this filter + final bool reachedOldest; + + /// When we determined reachedOldest (null if not reached) + final int? reachedOldestAt; + + const FilterCoverageRecord({ + required this.filterHash, + required this.relayUrl, + required this.rangeStart, + required this.rangeEnd, + required this.fetchedAt, + this.reachedOldest = false, + this.reachedOldestAt, + }); + + /// Create a unique key for this record + String get key => '$filterHash:$relayUrl:$rangeStart'; + + @override + String toString() => + 'FilterCoverageRecord($filterHash, $relayUrl, $rangeStart-$rangeEnd)'; + + Map toJson() => { + 'filterHash': filterHash, + 'relayUrl': relayUrl, + 'rangeStart': rangeStart, + 'rangeEnd': rangeEnd, + 'fetchedAt': fetchedAt, + 'reachedOldest': reachedOldest, + 'reachedOldestAt': reachedOldestAt, + }; + + factory FilterCoverageRecord.fromJson(Map json) => + FilterCoverageRecord( + filterHash: json['filterHash'] as String, + relayUrl: json['relayUrl'] as String, + rangeStart: json['rangeStart'] as int, + rangeEnd: json['rangeEnd'] as int, + fetchedAt: json['fetchedAt'] as int, + reachedOldest: json['reachedOldest'] as bool? ?? false, + reachedOldestAt: json['reachedOldestAt'] as int?, + ); +} + +/// Utility to generate a hash for a filter (excluding temporal fields) +class FilterFingerprint { + /// Generate a stable hash for a filter, excluding since/until/limit + static String generate(Filter filter) { + final map = {}; + + if (filter.ids != null && filter.ids!.isNotEmpty) { + map['ids'] = List.from(filter.ids!)..sort(); + } + if (filter.authors != null && filter.authors!.isNotEmpty) { + map['authors'] = List.from(filter.authors!)..sort(); + } + if (filter.kinds != null && filter.kinds!.isNotEmpty) { + map['kinds'] = List.from(filter.kinds!)..sort(); + } + if (filter.search != null) { + map['search'] = filter.search; + } + if (filter.tags != null && filter.tags!.isNotEmpty) { + final sortedTags = >{}; + final keys = filter.tags!.keys.toList()..sort(); + for (final key in keys) { + sortedTags[key] = List.from(filter.tags![key]!)..sort(); + } + map['tags'] = sortedTags; + } + + final jsonStr = jsonEncode(map); + final bytes = utf8.encode(jsonStr); + final digest = sha256.convert(bytes); + return digest.toString().substring(0, 16); // First 16 chars for brevity + } +} diff --git a/packages/ndk/lib/domain_layer/repositories/cache_manager.dart b/packages/ndk/lib/domain_layer/repositories/cache_manager.dart index 227a9ae54..b5c760ab3 100644 --- a/packages/ndk/lib/domain_layer/repositories/cache_manager.dart +++ b/packages/ndk/lib/domain_layer/repositories/cache_manager.dart @@ -1,4 +1,5 @@ import '../entities/contact_list.dart'; +import '../entities/filter_coverage.dart'; import '../entities/nip_01_event.dart'; import '../entities/nip_05.dart'; import '../entities/relay_set.dart'; @@ -79,4 +80,35 @@ abstract class CacheManager { Future> loadNip05s(List pubKeys); Future removeNip05(String pubKey); Future removeAllNip05s(); + + // ===================== + // Filter Coverage + // ===================== + + /// Save a filter coverage record + Future saveFilterCoverageRecord(FilterCoverageRecord record); + + /// Save multiple filter coverage records + Future saveFilterCoverageRecords(List records); + + /// Load all coverage records for a filter hash + Future> loadFilterCoverageRecords( + String filterHash); + + /// Load all coverage records for a filter hash and relay + Future> loadFilterCoverageRecordsByRelay( + String filterHash, String relayUrl); + + /// Load all coverage records for a relay (all filters) + Future> loadFilterCoverageRecordsByRelayUrl( + String relayUrl); + + /// Remove all coverage records for a filter hash + Future removeFilterCoverageRecords(String filterHash); + + /// Remove all coverage records for a relay + Future removeFilterCoverageRecordsByRelay(String relayUrl); + + /// Remove all filter coverage records + Future removeAllFilterCoverageRecords(); } diff --git a/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart b/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart new file mode 100644 index 000000000..948e2b51b --- /dev/null +++ b/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart @@ -0,0 +1,272 @@ +import '../../entities/filter.dart'; +import '../../entities/filter_coverage.dart'; +import '../../repositories/cache_manager.dart'; + +/// Usecase to track and query filter coverage per relay +class Coverage { + final CacheManager _cacheManager; + + Coverage({ + required CacheManager cacheManager, + }) : _cacheManager = cacheManager; + + /// Get coverage for a filter across all relays + Future> getForFilter(Filter filter) async { + final filterHash = FilterFingerprint.generate(filter); + final records = await _cacheManager.loadFilterCoverageRecords(filterHash); + + return _buildCoverageMap(filter, records); + } + + /// Get all coverages for a relay (all filters) + Future> getForRelay(String relayUrl) async { + final records = + await _cacheManager.loadFilterCoverageRecordsByRelayUrl(relayUrl); + + // Group by filterHash + final grouped = >{}; + for (final record in records) { + grouped.putIfAbsent(record.filterHash, () => []).add(record); + } + + // Build RelayCoverage for each filter + // Note: We don't have the original filter, so we create an empty one + // The filterHash is preserved but the filter details are not available + final result = []; + for (final entry in grouped.entries) { + final relayRecords = + entry.value.where((r) => r.relayUrl == relayUrl).toList(); + if (relayRecords.isNotEmpty) { + result.add(_buildRelayCoverage( + relayUrl, + Filter(), // Empty filter - we only have the hash + relayRecords, + )); + } + } + + return result; + } + + /// Find gaps in coverage for a filter within a time range + Future> findGaps({ + required Filter filter, + required int since, + required int until, + }) async { + final coverageMap = await getForFilter(filter); + final gaps = []; + + for (final entry in coverageMap.entries) { + gaps.addAll(entry.value.getGaps(since, until)); + } + + return gaps; + } + + /// Get optimized filters for each relay to fill gaps + /// Returns a map of relay URL to list of filters covering only the gaps + Future>> getOptimizedFilters({ + required Filter filter, + required int since, + required int until, + }) async { + final coverageMap = await getForFilter(filter); + final result = >{}; + + for (final entry in coverageMap.entries) { + final relayUrl = entry.key; + final coverage = entry.value; + + final gaps = coverage.findGaps(since, until); + if (gaps.isNotEmpty) { + result[relayUrl] = gaps.map((gap) { + final gapFilter = filter.clone(); + gapFilter.since = gap.since; + gapFilter.until = gap.until; + return gapFilter; + }).toList(); + } + } + + return result; + } + + /// Add a coverage range for a filter/relay combination + /// Automatically merges with existing adjacent/overlapping ranges + Future addRange({ + required Filter filter, + required String relayUrl, + required int since, + required int until, + }) async { + final filterHash = FilterFingerprint.generate(filter); + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + // Load existing records for this filter/relay + final existingRecords = await _cacheManager + .loadFilterCoverageRecordsByRelay(filterHash, relayUrl); + + // Convert to TimeRanges for merging + final ranges = existingRecords + .map((r) => TimeRange( + since: r.rangeStart, + until: r.rangeEnd, + fetchedAt: r.fetchedAt, + )) + .toList(); + + // Add the new range + ranges.add(TimeRange(since: since, until: until, fetchedAt: now)); + + // Merge overlapping/adjacent ranges + final mergedRanges = _mergeRanges(ranges); + + // Check if any existing record had reachedOldest + final reachedOldestRecord = existingRecords + .where((r) => r.reachedOldest) + .fold(null, (prev, r) => r); + + // Delete old records and save merged ones + await _cacheManager.removeFilterCoverageRecords(filterHash); + + final newRecords = mergedRanges.map((range) { + return FilterCoverageRecord( + filterHash: filterHash, + relayUrl: relayUrl, + rangeStart: range.since, + rangeEnd: range.until, + fetchedAt: range.fetchedAt, + reachedOldest: reachedOldestRecord?.reachedOldest ?? false, + reachedOldestAt: reachedOldestRecord?.reachedOldestAt, + ); + }).toList(); + + await _cacheManager.saveFilterCoverageRecords(newRecords); + } + + /// Mark that we've reached the oldest data for a filter/relay + Future markReachedOldest({ + required Filter filter, + required String relayUrl, + }) async { + final filterHash = FilterFingerprint.generate(filter); + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + final existingRecords = await _cacheManager + .loadFilterCoverageRecordsByRelay(filterHash, relayUrl); + + if (existingRecords.isEmpty) return; + + // Update all records for this filter/relay with reachedOldest + final updatedRecords = existingRecords.map((r) { + return FilterCoverageRecord( + filterHash: r.filterHash, + relayUrl: r.relayUrl, + rangeStart: r.rangeStart, + rangeEnd: r.rangeEnd, + fetchedAt: r.fetchedAt, + reachedOldest: true, + reachedOldestAt: now, + ); + }).toList(); + + // Remove old and save updated + for (final record in existingRecords) { + await _cacheManager.removeFilterCoverageRecords(record.filterHash); + } + await _cacheManager.saveFilterCoverageRecords(updatedRecords); + } + + /// Clear coverage for a specific filter + Future clearForFilter(Filter filter) async { + final filterHash = FilterFingerprint.generate(filter); + await _cacheManager.removeFilterCoverageRecords(filterHash); + } + + /// Clear all coverage records for a relay + Future clearForRelay(String relayUrl) async { + await _cacheManager.removeFilterCoverageRecordsByRelay(relayUrl); + } + + /// Clear all coverage records + Future clearAll() async { + await _cacheManager.removeAllFilterCoverageRecords(); + } + + // ===================== + // Private helpers + // ===================== + + /// Build a map of relay URL to RelayCoverage from records + Map _buildCoverageMap( + Filter filter, + List records, + ) { + // Group by relay + final grouped = >{}; + for (final record in records) { + grouped.putIfAbsent(record.relayUrl, () => []).add(record); + } + + // Build RelayCoverage for each relay + final result = {}; + for (final entry in grouped.entries) { + result[entry.key] = _buildRelayCoverage(entry.key, filter, entry.value); + } + + return result; + } + + /// Build a RelayCoverage from records for a single relay + RelayCoverage _buildRelayCoverage( + String relayUrl, + Filter filter, + List records, + ) { + final ranges = records + .map((r) => TimeRange( + since: r.rangeStart, + until: r.rangeEnd, + fetchedAt: r.fetchedAt, + )) + .toList() + ..sort((a, b) => a.since.compareTo(b.since)); + + // Check if any record has reachedOldest + final reachedOldestRecord = + records.where((r) => r.reachedOldest).firstOrNull; + + return RelayCoverage( + relayUrl: relayUrl, + filter: filter, + ranges: ranges, + reachedOldestAt: reachedOldestRecord?.reachedOldestAt, + ); + } + + /// Merge overlapping and adjacent time ranges + List _mergeRanges(List ranges) { + if (ranges.isEmpty) return []; + + // Sort by since + final sorted = List.from(ranges) + ..sort((a, b) => a.since.compareTo(b.since)); + + final merged = []; + var current = sorted.first; + + for (var i = 1; i < sorted.length; i++) { + final next = sorted[i]; + if (current.canMergeWith(next)) { + current = current.mergeWith(next); + } else { + merged.add(current); + current = next; + } + } + merged.add(current); + + return merged; + } +} diff --git a/packages/ndk/lib/ndk.dart b/packages/ndk/lib/ndk.dart index 44edab661..3e126ba90 100644 --- a/packages/ndk/lib/ndk.dart +++ b/packages/ndk/lib/ndk.dart @@ -81,6 +81,8 @@ export 'domain_layer/usecases/gift_wrap/gift_wrap.dart'; export 'domain_layer/usecases/bunkers/bunkers.dart'; export 'domain_layer/usecases/bunkers/models/bunker_connection.dart'; export 'domain_layer/usecases/bunkers/models/nostr_connect.dart'; +export 'domain_layer/usecases/coverage/coverage.dart'; +export 'domain_layer/entities/filter_coverage.dart'; /** * other stuff diff --git a/packages/ndk/lib/presentation_layer/init.dart b/packages/ndk/lib/presentation_layer/init.dart index 8abafed0c..7cb7d4cc2 100644 --- a/packages/ndk/lib/presentation_layer/init.dart +++ b/packages/ndk/lib/presentation_layer/init.dart @@ -15,6 +15,7 @@ import '../domain_layer/usecases/accounts/accounts.dart'; import '../domain_layer/usecases/broadcast/broadcast.dart'; import '../domain_layer/usecases/bunkers/bunkers.dart'; import '../domain_layer/usecases/cache_read/cache_read.dart'; +import '../domain_layer/usecases/coverage/coverage.dart'; import '../domain_layer/usecases/cache_write/cache_write.dart'; import '../domain_layer/usecases/connectivity/connectivity.dart'; import '../domain_layer/usecases/engines/network_engine.dart'; @@ -79,6 +80,7 @@ class Initialization { late Search search; late GiftWrap giftWrap; late Connectivy connectivity; + late Coverage coverage; late VerifyNip05 verifyNip05; @@ -239,6 +241,10 @@ class Initialization { requests: requests, ); + coverage = Coverage( + cacheManager: _ndkConfig.cache, + ); + giftWrap = GiftWrap(accounts: accounts); connectivity = Connectivy(relayManager); diff --git a/packages/ndk/lib/presentation_layer/ndk.dart b/packages/ndk/lib/presentation_layer/ndk.dart index c37528ae7..eca124e9c 100644 --- a/packages/ndk/lib/presentation_layer/ndk.dart +++ b/packages/ndk/lib/presentation_layer/ndk.dart @@ -7,6 +7,7 @@ import '../domain_layer/usecases/accounts/accounts.dart'; import '../domain_layer/usecases/broadcast/broadcast.dart'; import '../domain_layer/usecases/bunkers/bunkers.dart'; import '../domain_layer/usecases/connectivity/connectivity.dart'; +import '../domain_layer/usecases/coverage/coverage.dart'; import '../domain_layer/usecases/files/blossom.dart'; import '../domain_layer/usecases/files/blossom_user_server_list.dart'; import '../domain_layer/usecases/files/files.dart'; @@ -147,6 +148,10 @@ class Ndk { @experimental Search get search => _initialization.search; + /// Filter coverage tracking + /// Track which time ranges have been fetched from which relays for each filter + Coverage get coverage => _initialization.coverage; + /// Close all transports on relay manager Future destroy() async { final allFutures = [ From 26481399c8b085274f3daa1c74fde59a66fc92ef Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 7 Jan 2026 14:06:42 +0100 Subject: [PATCH 02/13] feat: automatic coverage --- .../usecases/requests/requests.dart | 37 +++++++++++++++++++ packages/ndk/lib/presentation_layer/init.dart | 3 ++ 2 files changed, 40 insertions(+) diff --git a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart index 20c2b18da..f9c90ebe1 100644 --- a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart +++ b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart @@ -14,6 +14,7 @@ import '../../entities/request_state.dart'; import '../../repositories/event_verifier.dart'; import '../cache_read/cache_read.dart'; import '../cache_write/cache_write.dart'; +import '../coverage/coverage.dart'; import '../engines/network_engine.dart'; import '../relay_manager.dart'; import '../stream_response_cleaner/stream_response_cleaner.dart'; @@ -30,6 +31,7 @@ class Requests { final EventVerifier _eventVerifier; final List _eventOutFilters; final Duration _defaultQueryTimeout; + Coverage? _coverage; /// Creates a new [Requests] instance /// @@ -56,6 +58,9 @@ class Requests { _eventOutFilters = eventOutFilters, _defaultQueryTimeout = defaultQueryTimeout; + /// Set the coverage tracker for automatic range recording + set coverage(Coverage? coverage) => _coverage = coverage; + /// Performs a low-level Nostr query /// /// [filters] A list of filters to apply to the query \ @@ -220,6 +225,11 @@ class Requests { eventOutFilters: _eventOutFilters, )(); + // Record coverage when network requests complete (EOSE received) + state.networkController.done.then((_) { + _recordCoverage(state); + }); + // cleanup on close // use done future for replay subject state.controller.done.then((_) { @@ -261,4 +271,31 @@ class Requests { // Return the response immediately return response; } + + /// Records coverage for each relay that received EOSE + void _recordCoverage(RequestState state) { + if (_coverage == null) return; + + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + for (final entry in state.requests.entries) { + final relayUrl = entry.key; + final relayState = entry.value; + + if (!relayState.receivedEOSE) continue; + + // Record coverage for each filter sent to this relay + for (final filter in relayState.filters) { + final since = filter.since ?? 0; + final until = filter.until ?? now; + + _coverage!.addRange( + filter: filter, + relayUrl: relayUrl, + since: since, + until: until, + ); + } + } + } } diff --git a/packages/ndk/lib/presentation_layer/init.dart b/packages/ndk/lib/presentation_layer/init.dart index 7cb7d4cc2..b59567478 100644 --- a/packages/ndk/lib/presentation_layer/init.dart +++ b/packages/ndk/lib/presentation_layer/init.dart @@ -245,6 +245,9 @@ class Initialization { cacheManager: _ndkConfig.cache, ); + // Connect coverage to requests for automatic range recording + requests.coverage = coverage; + giftWrap = GiftWrap(accounts: accounts); connectivity = Connectivy(relayManager); From 057bd76916b80a2ecb64e8687f0375df110b47c6 Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 7 Jan 2026 15:08:18 +0100 Subject: [PATCH 03/13] feat: unit tests --- .../cache_manager/mem_cache_manager.dart | 7 + .../repositories/cache_manager.dart | 4 + .../usecases/coverage/coverage.dart | 5 +- .../test/entities/filter_coverage_test.dart | 352 +++++++++++++ .../test/usecases/coverage/coverage_test.dart | 469 ++++++++++++++++++ 5 files changed, 835 insertions(+), 2 deletions(-) create mode 100644 packages/ndk/test/entities/filter_coverage_test.dart create mode 100644 packages/ndk/test/usecases/coverage/coverage_test.dart diff --git a/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart b/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart index e2de7ad3b..0849d2418 100644 --- a/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart +++ b/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart @@ -350,6 +350,13 @@ class MemCacheManager implements CacheManager { .removeWhere((key, value) => value.filterHash == filterHash); } + @override + Future removeFilterCoverageRecordsByFilterAndRelay( + String filterHash, String relayUrl) async { + filterCoverageRecords.removeWhere((key, value) => + value.filterHash == filterHash && value.relayUrl == relayUrl); + } + @override Future removeFilterCoverageRecordsByRelay(String relayUrl) async { filterCoverageRecords diff --git a/packages/ndk/lib/domain_layer/repositories/cache_manager.dart b/packages/ndk/lib/domain_layer/repositories/cache_manager.dart index b5c760ab3..d79084412 100644 --- a/packages/ndk/lib/domain_layer/repositories/cache_manager.dart +++ b/packages/ndk/lib/domain_layer/repositories/cache_manager.dart @@ -106,6 +106,10 @@ abstract class CacheManager { /// Remove all coverage records for a filter hash Future removeFilterCoverageRecords(String filterHash); + /// Remove coverage records for a specific filter hash and relay + Future removeFilterCoverageRecordsByFilterAndRelay( + String filterHash, String relayUrl); + /// Remove all coverage records for a relay Future removeFilterCoverageRecordsByRelay(String relayUrl); diff --git a/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart b/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart index 948e2b51b..70158b238 100644 --- a/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart +++ b/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart @@ -127,8 +127,9 @@ class Coverage { .where((r) => r.reachedOldest) .fold(null, (prev, r) => r); - // Delete old records and save merged ones - await _cacheManager.removeFilterCoverageRecords(filterHash); + // Delete old records for this filter/relay and save merged ones + await _cacheManager.removeFilterCoverageRecordsByFilterAndRelay( + filterHash, relayUrl); final newRecords = mergedRanges.map((range) { return FilterCoverageRecord( diff --git a/packages/ndk/test/entities/filter_coverage_test.dart b/packages/ndk/test/entities/filter_coverage_test.dart new file mode 100644 index 000000000..04a9d91bd --- /dev/null +++ b/packages/ndk/test/entities/filter_coverage_test.dart @@ -0,0 +1,352 @@ +import 'package:test/test.dart'; +import 'package:ndk/ndk.dart'; + +void main() { + group('TimeRange', () { + test('constructor initializes correctly', () { + final range = TimeRange( + since: 1704067200, + until: 1704153600, + fetchedAt: 1704160000, + ); + + expect(range.since, 1704067200); + expect(range.until, 1704153600); + expect(range.fetchedAt, 1704160000); + }); + + test('canMergeWith detects overlapping ranges', () { + final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); + final range2 = TimeRange(since: 150, until: 250, fetchedAt: 1000); + + expect(range1.canMergeWith(range2), isTrue); + expect(range2.canMergeWith(range1), isTrue); + }); + + test('canMergeWith detects adjacent ranges', () { + final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); + final range2 = TimeRange(since: 201, until: 300, fetchedAt: 1000); + + expect(range1.canMergeWith(range2), isTrue); + expect(range2.canMergeWith(range1), isTrue); + }); + + test('canMergeWith returns false for non-adjacent ranges', () { + final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); + final range2 = TimeRange(since: 300, until: 400, fetchedAt: 1000); + + expect(range1.canMergeWith(range2), isFalse); + expect(range2.canMergeWith(range1), isFalse); + }); + + test('mergeWith combines ranges correctly', () { + final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); + final range2 = TimeRange(since: 150, until: 300, fetchedAt: 2000); + + final merged = range1.mergeWith(range2); + + expect(merged.since, 100); + expect(merged.until, 300); + expect(merged.fetchedAt, 2000); // Takes the most recent fetchedAt + }); + + test('contains checks if range fully contains a period', () { + final range = TimeRange(since: 100, until: 300, fetchedAt: 1000); + + expect(range.contains(150, 250), isTrue); + expect(range.contains(100, 300), isTrue); + expect(range.contains(50, 200), isFalse); + expect(range.contains(200, 400), isFalse); + }); + + test('overlaps checks if range overlaps with a period', () { + final range = TimeRange(since: 100, until: 300, fetchedAt: 1000); + + expect(range.overlaps(150, 250), isTrue); + expect(range.overlaps(50, 150), isTrue); + expect(range.overlaps(250, 400), isTrue); + expect(range.overlaps(400, 500), isFalse); + expect(range.overlaps(0, 50), isFalse); + }); + + test('toJson and fromJson work correctly', () { + final range = TimeRange( + since: 1704067200, + until: 1704153600, + fetchedAt: 1704160000, + ); + + final json = range.toJson(); + final restored = TimeRange.fromJson(json); + + expect(restored.since, range.since); + expect(restored.until, range.until); + expect(restored.fetchedAt, range.fetchedAt); + }); + + test('equality works correctly', () { + final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); + final range2 = TimeRange(since: 100, until: 200, fetchedAt: 1000); + final range3 = TimeRange(since: 100, until: 300, fetchedAt: 1000); + + expect(range1, equals(range2)); + expect(range1, isNot(equals(range3))); + }); + }); + + group('RelayCoverage', () { + test('oldest and newest return correct values', () { + final coverage = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [ + TimeRange(since: 200, until: 300, fetchedAt: 1000), + TimeRange(since: 100, until: 150, fetchedAt: 1000), + TimeRange(since: 500, until: 600, fetchedAt: 1000), + ], + ); + + expect(coverage.oldest, 100); + expect(coverage.newest, 600); + }); + + test('oldest and newest return null for empty ranges', () { + final coverage = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [], + ); + + expect(coverage.oldest, isNull); + expect(coverage.newest, isNull); + }); + + test('reachedOldest getter works correctly', () { + final coverageNotReached = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [], + reachedOldestAt: null, + ); + + final coverageReached = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [], + reachedOldestAt: 1704160000, + ); + + expect(coverageNotReached.reachedOldest, isFalse); + expect(coverageReached.reachedOldest, isTrue); + }); + + test('findGaps returns correct gaps for empty ranges', () { + final coverage = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [], + ); + + final gaps = coverage.findGaps(100, 500); + + expect(gaps.length, 1); + expect(gaps[0].since, 100); + expect(gaps[0].until, 500); + }); + + test('findGaps returns correct gaps for single range', () { + final coverage = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [ + TimeRange(since: 200, until: 300, fetchedAt: 1000), + ], + ); + + final gaps = coverage.findGaps(100, 500); + + expect(gaps.length, 2); + expect(gaps[0].since, 100); + expect(gaps[0].until, 199); + expect(gaps[1].since, 301); + expect(gaps[1].until, 500); + }); + + test('findGaps returns correct gaps for multiple ranges', () { + // Ranges: [100-150], [300-400] + // Query: 0 to 600 + // Expected gaps: [0-99], [151-299], [401-600] + final coverage = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [ + TimeRange(since: 100, until: 150, fetchedAt: 1000), + TimeRange(since: 300, until: 400, fetchedAt: 1000), + ], + ); + + final gaps = coverage.findGaps(0, 600); + + expect(gaps.length, 3); + expect(gaps[0].since, 0); + expect(gaps[0].until, 99); + expect(gaps[1].since, 151); + expect(gaps[1].until, 299); + expect(gaps[2].since, 401); + expect(gaps[2].until, 600); + }); + + test('findGaps returns empty list when fully covered', () { + final coverage = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [ + TimeRange(since: 100, until: 500, fetchedAt: 1000), + ], + ); + + final gaps = coverage.findGaps(200, 400); + + expect(gaps, isEmpty); + }); + + test('getGaps returns CoverageGap objects', () { + final coverage = RelayCoverage( + relayUrl: 'wss://relay.example.com', + filter: Filter(kinds: [1]), + ranges: [ + TimeRange(since: 200, until: 300, fetchedAt: 1000), + ], + ); + + final gaps = coverage.getGaps(100, 500); + + expect(gaps.length, 2); + expect(gaps[0].relayUrl, 'wss://relay.example.com'); + expect(gaps[0].since, 100); + expect(gaps[0].until, 199); + }); + }); + + group('FilterCoverageRecord', () { + test('constructor initializes correctly', () { + final record = FilterCoverageRecord( + filterHash: 'abc123', + relayUrl: 'wss://relay.example.com', + rangeStart: 1704067200, + rangeEnd: 1704153600, + fetchedAt: 1704160000, + reachedOldest: true, + reachedOldestAt: 1704160000, + ); + + expect(record.filterHash, 'abc123'); + expect(record.relayUrl, 'wss://relay.example.com'); + expect(record.rangeStart, 1704067200); + expect(record.rangeEnd, 1704153600); + expect(record.fetchedAt, 1704160000); + expect(record.reachedOldest, isTrue); + expect(record.reachedOldestAt, 1704160000); + }); + + test('key is generated correctly', () { + final record = FilterCoverageRecord( + filterHash: 'abc123', + relayUrl: 'wss://relay.example.com', + rangeStart: 1704067200, + rangeEnd: 1704153600, + fetchedAt: 1704160000, + ); + + expect(record.key, 'abc123:wss://relay.example.com:1704067200'); + }); + + test('toJson and fromJson work correctly', () { + final record = FilterCoverageRecord( + filterHash: 'abc123', + relayUrl: 'wss://relay.example.com', + rangeStart: 1704067200, + rangeEnd: 1704153600, + fetchedAt: 1704160000, + reachedOldest: true, + reachedOldestAt: 1704155000, + ); + + final json = record.toJson(); + final restored = FilterCoverageRecord.fromJson(json); + + expect(restored.filterHash, record.filterHash); + expect(restored.relayUrl, record.relayUrl); + expect(restored.rangeStart, record.rangeStart); + expect(restored.rangeEnd, record.rangeEnd); + expect(restored.fetchedAt, record.fetchedAt); + expect(restored.reachedOldest, record.reachedOldest); + expect(restored.reachedOldestAt, record.reachedOldestAt); + }); + }); + + group('FilterFingerprint', () { + test('generates same hash for identical filters', () { + final filter1 = Filter(kinds: [1], authors: ['pubkey1', 'pubkey2']); + final filter2 = Filter(kinds: [1], authors: ['pubkey1', 'pubkey2']); + + final hash1 = FilterFingerprint.generate(filter1); + final hash2 = FilterFingerprint.generate(filter2); + + expect(hash1, equals(hash2)); + }); + + test('generates same hash regardless of author order', () { + final filter1 = Filter(kinds: [1], authors: ['pubkey1', 'pubkey2']); + final filter2 = Filter(kinds: [1], authors: ['pubkey2', 'pubkey1']); + + final hash1 = FilterFingerprint.generate(filter1); + final hash2 = FilterFingerprint.generate(filter2); + + expect(hash1, equals(hash2)); + }); + + test('ignores since/until/limit in hash', () { + final filter1 = Filter(kinds: [1], authors: ['pubkey1']); + final filter2 = Filter( + kinds: [1], + authors: ['pubkey1'], + since: 1704067200, + until: 1704153600, + limit: 100, + ); + + final hash1 = FilterFingerprint.generate(filter1); + final hash2 = FilterFingerprint.generate(filter2); + + expect(hash1, equals(hash2)); + }); + + test('generates different hash for different filters', () { + final filter1 = Filter(kinds: [1], authors: ['pubkey1']); + final filter2 = Filter(kinds: [1], authors: ['pubkey2']); + final filter3 = Filter(kinds: [0], authors: ['pubkey1']); + + final hash1 = FilterFingerprint.generate(filter1); + final hash2 = FilterFingerprint.generate(filter2); + final hash3 = FilterFingerprint.generate(filter3); + + expect(hash1, isNot(equals(hash2))); + expect(hash1, isNot(equals(hash3))); + }); + }); + + group('CoverageGap', () { + test('constructor initializes correctly', () { + final gap = CoverageGap( + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + expect(gap.relayUrl, 'wss://relay.example.com'); + expect(gap.since, 100); + expect(gap.until, 200); + }); + }); +} diff --git a/packages/ndk/test/usecases/coverage/coverage_test.dart b/packages/ndk/test/usecases/coverage/coverage_test.dart new file mode 100644 index 000000000..6606f5eab --- /dev/null +++ b/packages/ndk/test/usecases/coverage/coverage_test.dart @@ -0,0 +1,469 @@ +import 'package:test/test.dart'; +import 'package:ndk/ndk.dart'; + +void main() { + late MemCacheManager cacheManager; + late Coverage coverage; + + setUp(() { + cacheManager = MemCacheManager(); + coverage = Coverage(cacheManager: cacheManager); + }); + + group('Coverage.addRange', () { + test('adds a new range for a filter/relay', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + final result = await coverage.getForFilter(filter); + + expect(result.containsKey('wss://relay.example.com'), isTrue); + expect(result['wss://relay.example.com']!.ranges.length, 1); + expect(result['wss://relay.example.com']!.ranges[0].since, 100); + expect(result['wss://relay.example.com']!.ranges[0].until, 200); + }); + + test('merges adjacent ranges', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 201, + until: 300, + ); + + final result = await coverage.getForFilter(filter); + + expect(result['wss://relay.example.com']!.ranges.length, 1); + expect(result['wss://relay.example.com']!.ranges[0].since, 100); + expect(result['wss://relay.example.com']!.ranges[0].until, 300); + }); + + test('merges overlapping ranges', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 250, + ); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 200, + until: 400, + ); + + final result = await coverage.getForFilter(filter); + + expect(result['wss://relay.example.com']!.ranges.length, 1); + expect(result['wss://relay.example.com']!.ranges[0].since, 100); + expect(result['wss://relay.example.com']!.ranges[0].until, 400); + }); + + test('keeps separate ranges when not adjacent', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 400, + until: 500, + ); + + final result = await coverage.getForFilter(filter); + + expect(result['wss://relay.example.com']!.ranges.length, 2); + }); + + test('tracks different relays separately', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay1.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay2.example.com', + since: 150, + until: 300, + ); + + final result = await coverage.getForFilter(filter); + + expect(result.length, 2); + expect(result['wss://relay1.example.com']!.ranges[0].since, 100); + expect(result['wss://relay1.example.com']!.ranges[0].until, 200); + expect(result['wss://relay2.example.com']!.ranges[0].since, 150); + expect(result['wss://relay2.example.com']!.ranges[0].until, 300); + }); + + test('tracks different filters separately', () async { + final filter1 = Filter(kinds: [1], authors: ['pubkey1']); + final filter2 = Filter(kinds: [0], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter1, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter2, + relayUrl: 'wss://relay.example.com', + since: 300, + until: 400, + ); + + final result1 = await coverage.getForFilter(filter1); + final result2 = await coverage.getForFilter(filter2); + + expect(result1['wss://relay.example.com']!.ranges[0].since, 100); + expect(result2['wss://relay.example.com']!.ranges[0].since, 300); + }); + }); + + group('Coverage.findGaps', () { + test('returns full range as gap when no coverage', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + final gaps = await coverage.findGaps( + filter: filter, + since: 100, + until: 500, + ); + + expect(gaps, isEmpty); // No relays tracked, no gaps returned + }); + + test('returns gaps for partial coverage', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 200, + until: 300, + ); + + final gaps = await coverage.findGaps( + filter: filter, + since: 100, + until: 500, + ); + + expect(gaps.length, 2); + expect(gaps[0].since, 100); + expect(gaps[0].until, 199); + expect(gaps[1].since, 301); + expect(gaps[1].until, 500); + }); + + test('returns empty when fully covered', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 0, + until: 1000, + ); + + final gaps = await coverage.findGaps( + filter: filter, + since: 100, + until: 500, + ); + + expect(gaps, isEmpty); + }); + }); + + group('Coverage.getOptimizedFilters', () { + test('returns filters for gaps only', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 200, + until: 300, + ); + + final optimized = await coverage.getOptimizedFilters( + filter: filter, + since: 100, + until: 500, + ); + + expect(optimized.containsKey('wss://relay.example.com'), isTrue); + final filters = optimized['wss://relay.example.com']!; + expect(filters.length, 2); + + // First gap: 100-199 + expect(filters[0].since, 100); + expect(filters[0].until, 199); + expect(filters[0].kinds, [1]); + expect(filters[0].authors, ['pubkey1']); + + // Second gap: 301-500 + expect(filters[1].since, 301); + expect(filters[1].until, 500); + }); + + test('returns empty for fully covered relay', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 0, + until: 1000, + ); + + final optimized = await coverage.getOptimizedFilters( + filter: filter, + since: 100, + until: 500, + ); + + expect(optimized, isEmpty); + }); + }); + + group('Coverage.markReachedOldest', () { + test('marks filter/relay as reached oldest', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.markReachedOldest( + filter: filter, + relayUrl: 'wss://relay.example.com', + ); + + final result = await coverage.getForFilter(filter); + + expect(result['wss://relay.example.com']!.reachedOldest, isTrue); + expect(result['wss://relay.example.com']!.reachedOldestAt, isNotNull); + }); + }); + + group('Coverage.getForRelay', () { + test('returns all coverages for a relay', () async { + final filter1 = Filter(kinds: [1], authors: ['pubkey1']); + final filter2 = Filter(kinds: [0], authors: ['pubkey2']); + + await coverage.addRange( + filter: filter1, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter2, + relayUrl: 'wss://relay.example.com', + since: 300, + until: 400, + ); + + final result = await coverage.getForRelay('wss://relay.example.com'); + + expect(result.length, 2); + }); + }); + + group('Coverage.clearForFilter', () { + test('clears coverage for a specific filter', () async { + final filter1 = Filter(kinds: [1], authors: ['pubkey1']); + final filter2 = Filter(kinds: [0], authors: ['pubkey2']); + + await coverage.addRange( + filter: filter1, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter2, + relayUrl: 'wss://relay.example.com', + since: 300, + until: 400, + ); + + await coverage.clearForFilter(filter1); + + final result1 = await coverage.getForFilter(filter1); + final result2 = await coverage.getForFilter(filter2); + + expect(result1, isEmpty); + expect(result2.isNotEmpty, isTrue); + }); + }); + + group('Coverage.clearForRelay', () { + test('clears all coverage for a relay', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay1.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay2.example.com', + since: 100, + until: 200, + ); + + await coverage.clearForRelay('wss://relay1.example.com'); + + final result = await coverage.getForFilter(filter); + + expect(result.containsKey('wss://relay1.example.com'), isFalse); + expect(result.containsKey('wss://relay2.example.com'), isTrue); + }); + }); + + group('Coverage.clearAll', () { + test('clears all coverage', () async { + final filter1 = Filter(kinds: [1], authors: ['pubkey1']); + final filter2 = Filter(kinds: [0], authors: ['pubkey2']); + + await coverage.addRange( + filter: filter1, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter2, + relayUrl: 'wss://relay.example.com', + since: 300, + until: 400, + ); + + await coverage.clearAll(); + + final result1 = await coverage.getForFilter(filter1); + final result2 = await coverage.getForFilter(filter2); + + expect(result1, isEmpty); + expect(result2, isEmpty); + }); + }); + + group('Coverage realistic scenarios', () { + test('handles Jan-Mar + Jun-Sep scenario with Apr-May gap', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + // Add Jan-Mar coverage + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.damus.io', + since: 1704067200, // Jan 1 + until: 1711929599, // Mar 31 + ); + + // Add Jun-Sep coverage + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.damus.io', + since: 1717200000, // Jun 1 + until: 1727740799, // Sep 30 + ); + + final result = await coverage.getForFilter(filter); + final damusCoverage = result['wss://relay.damus.io']!; + + // Should have 2 separate ranges + expect(damusCoverage.ranges.length, 2); + + // Check gaps for Jan-Oct query + final gaps = await coverage.findGaps( + filter: filter, + since: 1704067200, // Jan 1 + until: 1730419199, // Oct 31 + ); + + // Should have gap for Apr-May and Oct + expect(gaps.length, 2); + }); + + test('fills gap and merges ranges', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + // Initial: two separate ranges + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, + ); + + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 400, + until: 500, + ); + + // Verify 2 ranges and a gap + var result = await coverage.getForFilter(filter); + expect(result['wss://relay.example.com']!.ranges.length, 2); + + // Fill the gap + await coverage.addRange( + filter: filter, + relayUrl: 'wss://relay.example.com', + since: 201, + until: 399, + ); + + // Should now be 1 merged range + result = await coverage.getForFilter(filter); + expect(result['wss://relay.example.com']!.ranges.length, 1); + expect(result['wss://relay.example.com']!.ranges[0].since, 100); + expect(result['wss://relay.example.com']!.ranges[0].until, 500); + }); + }); +} From 9d2532b7dd92ec1526ef00c76f7389183d0b3e5d Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 7 Jan 2026 15:39:50 +0100 Subject: [PATCH 04/13] feat: integration tests --- .../coverage/coverage_integration_test.dart | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 packages/ndk/test/usecases/coverage/coverage_integration_test.dart diff --git a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart new file mode 100644 index 000000000..cb5ba77bc --- /dev/null +++ b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart @@ -0,0 +1,263 @@ +import 'package:ndk/ndk.dart'; +import 'package:ndk/shared/nips/nip01/bip340.dart'; +import 'package:ndk/shared/nips/nip01/key_pair.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_relay.dart'; + +void main() async { + KeyPair key1 = Bip340.generatePrivateKey(); + + Nip01Event textNote(KeyPair key) { + Nip01Event event = Nip01Event( + kind: Nip01Event.kTextNodeKind, + pubKey: key.publicKey, + content: "test note", + tags: [], + createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000, + ); + event.sign(key.privateKey!); + return event; + } + + Map textNotes = {key1: textNote(key1)}; + + group('Coverage integration', () { + test('query automatically records coverage after EOSE', + timeout: const Timeout(Duration(seconds: 5)), () async { + // Setup mock relay + MockRelay relay1 = MockRelay( + name: "relay coverage test", + explicitPort: 4200, + signEvents: false, + ); + await relay1.startServer(textNotes: textNotes); + + // Setup NDK + final cache = MemCacheManager(); + final ndk = Ndk( + NdkConfig( + eventVerifier: Bip340EventVerifier(), + cache: cache, + engine: NdkEngine.RELAY_SETS, + bootstrapRelays: [relay1.url], + ), + ); + + await ndk.relays.seedRelaysConnected; + + // Define filter with time bounds + final since = DateTime.now() + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch ~/ + 1000; + final until = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + final filter = Filter( + kinds: [Nip01Event.kTextNodeKind], + authors: [key1.publicKey], + since: since, + until: until, + ); + + // Make query + final response = ndk.requests.query(filters: [filter]); + + // Wait for response to complete + await response.future; + + // Small delay to ensure coverage is recorded + await Future.delayed(const Duration(milliseconds: 100)); + + // Check coverage was recorded + final coverage = await ndk.coverage.getForFilter(filter); + + expect(coverage.isNotEmpty, isTrue, + reason: 'Coverage should be recorded after query'); + expect(coverage.containsKey(relay1.url), isTrue, + reason: 'Coverage should contain the relay URL'); + + final relayCoverage = coverage[relay1.url]!; + expect(relayCoverage.ranges.isNotEmpty, isTrue, + reason: 'Should have at least one range'); + expect(relayCoverage.ranges.first.since, equals(since), + reason: 'Range since should match filter since'); + expect(relayCoverage.ranges.first.until, equals(until), + reason: 'Range until should match filter until'); + + await relay1.stopServer(); + await ndk.destroy(); + }); + + test('multiple queries merge coverage ranges', + timeout: const Timeout(Duration(seconds: 5)), () async { + MockRelay relay1 = MockRelay( + name: "relay merge test", + explicitPort: 4201, + signEvents: false, + ); + await relay1.startServer(textNotes: textNotes); + + final cache = MemCacheManager(); + final ndk = Ndk( + NdkConfig( + eventVerifier: Bip340EventVerifier(), + cache: cache, + engine: NdkEngine.RELAY_SETS, + bootstrapRelays: [relay1.url], + ), + ); + + await ndk.relays.seedRelaysConnected; + + // First query: Jan 1-15 + final filter1 = Filter( + kinds: [Nip01Event.kTextNodeKind], + authors: [key1.publicKey], + since: 100, + until: 200, + ); + + final response1 = ndk.requests.query(filters: [filter1]); + await response1.future; + await Future.delayed(const Duration(milliseconds: 100)); + + // Second query: Jan 16-31 (adjacent) + final filter2 = Filter( + kinds: [Nip01Event.kTextNodeKind], + authors: [key1.publicKey], + since: 201, + until: 300, + ); + + final response2 = ndk.requests.query(filters: [filter2]); + await response2.future; + await Future.delayed(const Duration(milliseconds: 100)); + + // Check coverage was merged + final coverage = await ndk.coverage.getForFilter(filter1); + + expect(coverage.containsKey(relay1.url), isTrue); + + final relayCoverage = coverage[relay1.url]!; + // Should have merged into 1 range (100-300) + expect(relayCoverage.ranges.length, equals(1), + reason: 'Adjacent ranges should be merged'); + expect(relayCoverage.ranges.first.since, equals(100)); + expect(relayCoverage.ranges.first.until, equals(300)); + + await relay1.stopServer(); + await ndk.destroy(); + }); + + test('findGaps returns correct gaps after query', + timeout: const Timeout(Duration(seconds: 5)), () async { + MockRelay relay1 = MockRelay( + name: "relay gaps test", + explicitPort: 4202, + signEvents: false, + ); + await relay1.startServer(textNotes: textNotes); + + final cache = MemCacheManager(); + final ndk = Ndk( + NdkConfig( + eventVerifier: Bip340EventVerifier(), + cache: cache, + engine: NdkEngine.RELAY_SETS, + bootstrapRelays: [relay1.url], + ), + ); + + await ndk.relays.seedRelaysConnected; + + // Query for 200-300 + final filter = Filter( + kinds: [Nip01Event.kTextNodeKind], + authors: [key1.publicKey], + since: 200, + until: 300, + ); + + final response = ndk.requests.query(filters: [filter]); + await response.future; + await Future.delayed(const Duration(milliseconds: 100)); + + // Find gaps for 100-500 + final gaps = await ndk.coverage.findGaps( + filter: filter, + since: 100, + until: 500, + ); + + // Should have 2 gaps: 100-199 and 301-500 + expect(gaps.length, equals(2), reason: 'Should have 2 gaps'); + expect(gaps[0].since, equals(100)); + expect(gaps[0].until, equals(199)); + expect(gaps[1].since, equals(301)); + expect(gaps[1].until, equals(500)); + + await relay1.stopServer(); + await ndk.destroy(); + }); + + test('getOptimizedFilters returns filters for gaps only', + timeout: const Timeout(Duration(seconds: 5)), () async { + MockRelay relay1 = MockRelay( + name: "relay optimized test", + explicitPort: 4203, + signEvents: false, + ); + await relay1.startServer(textNotes: textNotes); + + final cache = MemCacheManager(); + final ndk = Ndk( + NdkConfig( + eventVerifier: Bip340EventVerifier(), + cache: cache, + engine: NdkEngine.RELAY_SETS, + bootstrapRelays: [relay1.url], + ), + ); + + await ndk.relays.seedRelaysConnected; + + // Query for 200-300 + final filter = Filter( + kinds: [Nip01Event.kTextNodeKind], + authors: [key1.publicKey], + since: 200, + until: 300, + ); + + final response = ndk.requests.query(filters: [filter]); + await response.future; + await Future.delayed(const Duration(milliseconds: 100)); + + // Get optimized filters for 100-500 + final optimized = await ndk.coverage.getOptimizedFilters( + filter: filter, + since: 100, + until: 500, + ); + + expect(optimized.containsKey(relay1.url), isTrue); + + final filters = optimized[relay1.url]!; + expect(filters.length, equals(2), reason: 'Should have 2 gap filters'); + + // First gap filter: 100-199 + expect(filters[0].since, equals(100)); + expect(filters[0].until, equals(199)); + expect(filters[0].kinds, equals([Nip01Event.kTextNodeKind])); + expect(filters[0].authors, equals([key1.publicKey])); + + // Second gap filter: 301-500 + expect(filters[1].since, equals(301)); + expect(filters[1].until, equals(500)); + + await relay1.stopServer(); + await ndk.destroy(); + }); + }); +} From e78548a633cc3dcd3e5b5b87e05e1c4c20728a84 Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 7 Jan 2026 15:53:47 +0100 Subject: [PATCH 05/13] fix: added ranges --- .../usecases/requests/requests.dart | 25 +++- .../coverage/coverage_integration_test.dart | 141 +++++++----------- 2 files changed, 75 insertions(+), 91 deletions(-) diff --git a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart index f9c90ebe1..15088e32e 100644 --- a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart +++ b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart @@ -7,6 +7,7 @@ import '../../entities/event_filter.dart'; import '../../entities/filter.dart'; import '../../entities/global_state.dart'; import '../../entities/ndk_request.dart'; +import '../../entities/nip_01_event.dart'; import '../../entities/relay_connectivity.dart'; import '../../entities/relay_set.dart'; import '../../entities/request_response.dart'; @@ -273,10 +274,21 @@ class Requests { } /// Records coverage for each relay that received EOSE + /// Uses actual event timestamps to determine coverage range void _recordCoverage(RequestState state) { if (_coverage == null) return; - final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + // Get all events from the replay subject + final events = state.controller.values.toList(); + if (events.isEmpty) return; + + // Group events by source relay + final eventsByRelay = >{}; + for (final event in events) { + for (final source in event.sources) { + eventsByRelay.putIfAbsent(source, () => []).add(event); + } + } for (final entry in state.requests.entries) { final relayUrl = entry.key; @@ -284,11 +296,16 @@ class Requests { if (!relayState.receivedEOSE) continue; + final relayEvents = eventsByRelay[relayUrl]; + if (relayEvents == null || relayEvents.isEmpty) continue; + + // Calculate min/max timestamps from actual events + final timestamps = relayEvents.map((e) => e.createdAt).toList(); + final since = timestamps.reduce((a, b) => a < b ? a : b); + final until = timestamps.reduce((a, b) => a > b ? a : b); + // Record coverage for each filter sent to this relay for (final filter in relayState.filters) { - final since = filter.since ?? 0; - final until = filter.until ?? now; - _coverage!.addRange( filter: filter, relayUrl: relayUrl, diff --git a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart index cb5ba77bc..8598b10bd 100644 --- a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart +++ b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart @@ -8,23 +8,25 @@ import '../../mocks/mock_relay.dart'; void main() async { KeyPair key1 = Bip340.generatePrivateKey(); - Nip01Event textNote(KeyPair key) { + Nip01Event textNoteWithTimestamp(KeyPair key, int timestamp) { Nip01Event event = Nip01Event( kind: Nip01Event.kTextNodeKind, pubKey: key.publicKey, - content: "test note", + content: "test note at $timestamp", tags: [], - createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000, + createdAt: timestamp, ); event.sign(key.privateKey!); return event; } - Map textNotes = {key1: textNote(key1)}; - group('Coverage integration', () { - test('query automatically records coverage after EOSE', + test('query automatically records coverage based on event timestamps', timeout: const Timeout(Duration(seconds: 5)), () async { + // Create event with specific timestamp + final event1 = textNoteWithTimestamp(key1, 150); + Map textNotes = {key1: event1}; + // Setup mock relay MockRelay relay1 = MockRelay( name: "relay coverage test", @@ -46,18 +48,9 @@ void main() async { await ndk.relays.seedRelaysConnected; - // Define filter with time bounds - final since = DateTime.now() - .subtract(const Duration(days: 1)) - .millisecondsSinceEpoch ~/ - 1000; - final until = DateTime.now().millisecondsSinceEpoch ~/ 1000; - final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], - since: since, - until: until, ); // Make query @@ -80,19 +73,25 @@ void main() async { final relayCoverage = coverage[relay1.url]!; expect(relayCoverage.ranges.isNotEmpty, isTrue, reason: 'Should have at least one range'); - expect(relayCoverage.ranges.first.since, equals(since), - reason: 'Range since should match filter since'); - expect(relayCoverage.ranges.first.until, equals(until), - reason: 'Range until should match filter until'); + + // Coverage should be based on the event's createdAt (150) + expect(relayCoverage.ranges.first.since, equals(150), + reason: 'Range since should match event timestamp'); + expect(relayCoverage.ranges.first.until, equals(150), + reason: 'Range until should match event timestamp (single event)'); await relay1.stopServer(); await ndk.destroy(); }); - test('multiple queries merge coverage ranges', + test('coverage reflects actual events received, not filter bounds', timeout: const Timeout(Duration(seconds: 5)), () async { + // Create event with timestamp 500 + final event1 = textNoteWithTimestamp(key1, 500); + Map textNotes = {key1: event1}; + MockRelay relay1 = MockRelay( - name: "relay merge test", + name: "relay bounds test", explicitPort: 4201, signEvents: false, ); @@ -110,50 +109,40 @@ void main() async { await ndk.relays.seedRelaysConnected; - // First query: Jan 1-15 - final filter1 = Filter( + // Query with wide bounds (100-1000), but event is at 500 + final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], since: 100, - until: 200, + until: 1000, ); - final response1 = ndk.requests.query(filters: [filter1]); - await response1.future; - await Future.delayed(const Duration(milliseconds: 100)); - - // Second query: Jan 16-31 (adjacent) - final filter2 = Filter( - kinds: [Nip01Event.kTextNodeKind], - authors: [key1.publicKey], - since: 201, - until: 300, - ); - - final response2 = ndk.requests.query(filters: [filter2]); - await response2.future; + final response = ndk.requests.query(filters: [filter]); + await response.future; await Future.delayed(const Duration(milliseconds: 100)); - // Check coverage was merged - final coverage = await ndk.coverage.getForFilter(filter1); + final coverage = await ndk.coverage.getForFilter(filter); expect(coverage.containsKey(relay1.url), isTrue); final relayCoverage = coverage[relay1.url]!; - // Should have merged into 1 range (100-300) - expect(relayCoverage.ranges.length, equals(1), - reason: 'Adjacent ranges should be merged'); - expect(relayCoverage.ranges.first.since, equals(100)); - expect(relayCoverage.ranges.first.until, equals(300)); + // Coverage should be 500-500 (the actual event), NOT 100-1000 + expect(relayCoverage.ranges.first.since, equals(500), + reason: 'Coverage should reflect event timestamp, not filter since'); + expect(relayCoverage.ranges.first.until, equals(500), + reason: 'Coverage should reflect event timestamp, not filter until'); await relay1.stopServer(); await ndk.destroy(); }); - test('findGaps returns correct gaps after query', + test('no coverage recorded when no events received', timeout: const Timeout(Duration(seconds: 5)), () async { + // Empty - no events + Map textNotes = {}; + MockRelay relay1 = MockRelay( - name: "relay gaps test", + name: "relay empty test", explicitPort: 4202, signEvents: false, ); @@ -171,44 +160,38 @@ void main() async { await ndk.relays.seedRelaysConnected; - // Query for 200-300 final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], - since: 200, - until: 300, + since: 100, + until: 200, ); final response = ndk.requests.query(filters: [filter]); await response.future; await Future.delayed(const Duration(milliseconds: 100)); - // Find gaps for 100-500 - final gaps = await ndk.coverage.findGaps( - filter: filter, - since: 100, - until: 500, - ); + final coverage = await ndk.coverage.getForFilter(filter); - // Should have 2 gaps: 100-199 and 301-500 - expect(gaps.length, equals(2), reason: 'Should have 2 gaps'); - expect(gaps[0].since, equals(100)); - expect(gaps[0].until, equals(199)); - expect(gaps[1].since, equals(301)); - expect(gaps[1].until, equals(500)); + // No events = no coverage recorded + expect(coverage.isEmpty, isTrue, + reason: 'No coverage should be recorded when no events received'); await relay1.stopServer(); await ndk.destroy(); }); - test('getOptimizedFilters returns filters for gaps only', + test('coverage uses event timestamp', timeout: const Timeout(Duration(seconds: 5)), () async { + // Create event with specific timestamp + final event1 = textNoteWithTimestamp(key1, 100); + MockRelay relay1 = MockRelay( - name: "relay optimized test", + name: "relay event timestamp test", explicitPort: 4203, signEvents: false, ); - await relay1.startServer(textNotes: textNotes); + await relay1.startServer(textNotes: {key1: event1}); final cache = MemCacheManager(); final ndk = Ndk( @@ -222,39 +205,23 @@ void main() async { await ndk.relays.seedRelaysConnected; - // Query for 200-300 final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], - since: 200, - until: 300, ); final response = ndk.requests.query(filters: [filter]); await response.future; await Future.delayed(const Duration(milliseconds: 100)); - // Get optimized filters for 100-500 - final optimized = await ndk.coverage.getOptimizedFilters( - filter: filter, - since: 100, - until: 500, - ); - - expect(optimized.containsKey(relay1.url), isTrue); - - final filters = optimized[relay1.url]!; - expect(filters.length, equals(2), reason: 'Should have 2 gap filters'); + final coverage = await ndk.coverage.getForFilter(filter); - // First gap filter: 100-199 - expect(filters[0].since, equals(100)); - expect(filters[0].until, equals(199)); - expect(filters[0].kinds, equals([Nip01Event.kTextNodeKind])); - expect(filters[0].authors, equals([key1.publicKey])); + expect(coverage.containsKey(relay1.url), isTrue); - // Second gap filter: 301-500 - expect(filters[1].since, equals(301)); - expect(filters[1].until, equals(500)); + final relayCoverage = coverage[relay1.url]!; + // With single event at timestamp 100 + expect(relayCoverage.ranges.first.since, equals(100)); + expect(relayCoverage.ranges.first.until, equals(100)); await relay1.stopServer(); await ndk.destroy(); From 289b9d371ea882c2fbbef137a06f109b32aa358e Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 7 Jan 2026 16:39:18 +0100 Subject: [PATCH 06/13] simplifying: remove fetchedAt, reachedOldestAt and more --- .../entities/filter_coverage.dart | 46 ++---------- .../usecases/coverage/coverage.dart | 51 +------------ .../usecases/requests/requests.dart | 31 +++++--- .../test/entities/filter_coverage_test.dart | 72 ++++++++----------- .../coverage/coverage_integration_test.dart | 13 ++-- .../test/usecases/coverage/coverage_test.dart | 21 ++++-- 6 files changed, 83 insertions(+), 151 deletions(-) diff --git a/packages/ndk/lib/domain_layer/entities/filter_coverage.dart b/packages/ndk/lib/domain_layer/entities/filter_coverage.dart index 33329a74d..c60727040 100644 --- a/packages/ndk/lib/domain_layer/entities/filter_coverage.dart +++ b/packages/ndk/lib/domain_layer/entities/filter_coverage.dart @@ -12,13 +12,9 @@ class TimeRange { /// End timestamp (inclusive) final int until; - /// When this range was fetched - final int fetchedAt; - const TimeRange({ required this.since, required this.until, - required this.fetchedAt, }); /// Check if this range overlaps or is adjacent to another range @@ -32,7 +28,6 @@ class TimeRange { return TimeRange( since: min(since, other.since), until: max(until, other.until), - fetchedAt: max(fetchedAt, other.fetchedAt), ); } @@ -47,29 +42,24 @@ class TimeRange { } @override - String toString() => 'TimeRange($since - $until, fetchedAt: $fetchedAt)'; + String toString() => 'TimeRange($since - $until)'; @override bool operator ==(Object other) => identical(this, other) || - other is TimeRange && - since == other.since && - until == other.until && - fetchedAt == other.fetchedAt; + other is TimeRange && since == other.since && until == other.until; @override - int get hashCode => since.hashCode ^ until.hashCode ^ fetchedAt.hashCode; + int get hashCode => since.hashCode ^ until.hashCode; Map toJson() => { 'since': since, 'until': until, - 'fetchedAt': fetchedAt, }; factory TimeRange.fromJson(Map json) => TimeRange( since: json['since'] as int, until: json['until'] as int, - fetchedAt: json['fetchedAt'] as int, ); } @@ -94,24 +84,22 @@ class RelayCoverage { final String relayUrl; final Filter filter; final List ranges; - final int? reachedOldestAt; const RelayCoverage({ required this.relayUrl, required this.filter, required this.ranges, - this.reachedOldestAt, }); - /// Whether we've confirmed there's no older data on this relay - bool get reachedOldest => reachedOldestAt != null; - /// Oldest timestamp we have data for (start of first range) int? get oldest { if (ranges.isEmpty) return null; return ranges.map((r) => r.since).reduce(min); } + /// Whether we've confirmed there's no older data on this relay + bool get reachedOldest => oldest == 0; + /// Newest timestamp we have data for (end of last range) int? get newest { if (ranges.isEmpty) return null; @@ -121,7 +109,7 @@ class RelayCoverage { /// Calculate gaps between ranges for a given time period List findGaps(int since, int until) { if (ranges.isEmpty) { - return [TimeRange(since: since, until: until, fetchedAt: 0)]; + return [TimeRange(since: since, until: until)]; } // Sort ranges by since @@ -142,7 +130,6 @@ class RelayCoverage { gaps.add(TimeRange( since: currentPos, until: min(range.since - 1, until), - fetchedAt: 0, )); } @@ -155,7 +142,6 @@ class RelayCoverage { gaps.add(TimeRange( since: currentPos, until: until, - fetchedAt: 0, )); } @@ -192,23 +178,11 @@ class FilterCoverageRecord { /// End of the covered range final int rangeEnd; - /// When this range was fetched - final int fetchedAt; - - /// Whether we've reached the oldest data on this relay for this filter - final bool reachedOldest; - - /// When we determined reachedOldest (null if not reached) - final int? reachedOldestAt; - const FilterCoverageRecord({ required this.filterHash, required this.relayUrl, required this.rangeStart, required this.rangeEnd, - required this.fetchedAt, - this.reachedOldest = false, - this.reachedOldestAt, }); /// Create a unique key for this record @@ -223,9 +197,6 @@ class FilterCoverageRecord { 'relayUrl': relayUrl, 'rangeStart': rangeStart, 'rangeEnd': rangeEnd, - 'fetchedAt': fetchedAt, - 'reachedOldest': reachedOldest, - 'reachedOldestAt': reachedOldestAt, }; factory FilterCoverageRecord.fromJson(Map json) => @@ -234,9 +205,6 @@ class FilterCoverageRecord { relayUrl: json['relayUrl'] as String, rangeStart: json['rangeStart'] as int, rangeEnd: json['rangeEnd'] as int, - fetchedAt: json['fetchedAt'] as int, - reachedOldest: json['reachedOldest'] as bool? ?? false, - reachedOldestAt: json['reachedOldestAt'] as int?, ); } diff --git a/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart b/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart index 70158b238..b2b83272c 100644 --- a/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart +++ b/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart @@ -101,7 +101,6 @@ class Coverage { required int until, }) async { final filterHash = FilterFingerprint.generate(filter); - final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; // Load existing records for this filter/relay final existingRecords = await _cacheManager @@ -112,21 +111,15 @@ class Coverage { .map((r) => TimeRange( since: r.rangeStart, until: r.rangeEnd, - fetchedAt: r.fetchedAt, )) .toList(); // Add the new range - ranges.add(TimeRange(since: since, until: until, fetchedAt: now)); + ranges.add(TimeRange(since: since, until: until)); // Merge overlapping/adjacent ranges final mergedRanges = _mergeRanges(ranges); - // Check if any existing record had reachedOldest - final reachedOldestRecord = existingRecords - .where((r) => r.reachedOldest) - .fold(null, (prev, r) => r); - // Delete old records for this filter/relay and save merged ones await _cacheManager.removeFilterCoverageRecordsByFilterAndRelay( filterHash, relayUrl); @@ -137,48 +130,12 @@ class Coverage { relayUrl: relayUrl, rangeStart: range.since, rangeEnd: range.until, - fetchedAt: range.fetchedAt, - reachedOldest: reachedOldestRecord?.reachedOldest ?? false, - reachedOldestAt: reachedOldestRecord?.reachedOldestAt, ); }).toList(); await _cacheManager.saveFilterCoverageRecords(newRecords); } - /// Mark that we've reached the oldest data for a filter/relay - Future markReachedOldest({ - required Filter filter, - required String relayUrl, - }) async { - final filterHash = FilterFingerprint.generate(filter); - final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; - - final existingRecords = await _cacheManager - .loadFilterCoverageRecordsByRelay(filterHash, relayUrl); - - if (existingRecords.isEmpty) return; - - // Update all records for this filter/relay with reachedOldest - final updatedRecords = existingRecords.map((r) { - return FilterCoverageRecord( - filterHash: r.filterHash, - relayUrl: r.relayUrl, - rangeStart: r.rangeStart, - rangeEnd: r.rangeEnd, - fetchedAt: r.fetchedAt, - reachedOldest: true, - reachedOldestAt: now, - ); - }).toList(); - - // Remove old and save updated - for (final record in existingRecords) { - await _cacheManager.removeFilterCoverageRecords(record.filterHash); - } - await _cacheManager.saveFilterCoverageRecords(updatedRecords); - } - /// Clear coverage for a specific filter Future clearForFilter(Filter filter) async { final filterHash = FilterFingerprint.generate(filter); @@ -229,20 +186,14 @@ class Coverage { .map((r) => TimeRange( since: r.rangeStart, until: r.rangeEnd, - fetchedAt: r.fetchedAt, )) .toList() ..sort((a, b) => a.since.compareTo(b.since)); - // Check if any record has reachedOldest - final reachedOldestRecord = - records.where((r) => r.reachedOldest).firstOrNull; - return RelayCoverage( relayUrl: relayUrl, filter: filter, ranges: ranges, - reachedOldestAt: reachedOldestRecord?.reachedOldestAt, ); } diff --git a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart index 15088e32e..94d658c18 100644 --- a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart +++ b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart @@ -274,13 +274,16 @@ class Requests { } /// Records coverage for each relay that received EOSE - /// Uses actual event timestamps to determine coverage range + /// - If events received: use min/max of event timestamps + /// - If no events + filter has since/until: use filter bounds + /// - If no events + no bounds: use 0 to now void _recordCoverage(RequestState state) { if (_coverage == null) return; + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + // Get all events from the replay subject final events = state.controller.values.toList(); - if (events.isEmpty) return; // Group events by source relay final eventsByRelay = >{}; @@ -297,15 +300,27 @@ class Requests { if (!relayState.receivedEOSE) continue; final relayEvents = eventsByRelay[relayUrl]; - if (relayEvents == null || relayEvents.isEmpty) continue; - - // Calculate min/max timestamps from actual events - final timestamps = relayEvents.map((e) => e.createdAt).toList(); - final since = timestamps.reduce((a, b) => a < b ? a : b); - final until = timestamps.reduce((a, b) => a > b ? a : b); // Record coverage for each filter sent to this relay for (final filter in relayState.filters) { + int since; + int until; + + if (relayEvents != null && relayEvents.isNotEmpty) { + // Use actual event timestamps + final timestamps = relayEvents.map((e) => e.createdAt).toList(); + since = timestamps.reduce((a, b) => a < b ? a : b); + until = timestamps.reduce((a, b) => a > b ? a : b); + } else if (filter.since != null || filter.until != null) { + // No events but filter has explicit bounds + since = filter.since ?? 0; + until = filter.until ?? now; + } else { + // No events, no bounds - relay has nothing, record 0 to now + since = 0; + until = now; + } + _coverage!.addRange( filter: filter, relayUrl: relayUrl, diff --git a/packages/ndk/test/entities/filter_coverage_test.dart b/packages/ndk/test/entities/filter_coverage_test.dart index 04a9d91bd..72639b419 100644 --- a/packages/ndk/test/entities/filter_coverage_test.dart +++ b/packages/ndk/test/entities/filter_coverage_test.dart @@ -7,51 +7,48 @@ void main() { final range = TimeRange( since: 1704067200, until: 1704153600, - fetchedAt: 1704160000, ); expect(range.since, 1704067200); expect(range.until, 1704153600); - expect(range.fetchedAt, 1704160000); }); test('canMergeWith detects overlapping ranges', () { - final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); - final range2 = TimeRange(since: 150, until: 250, fetchedAt: 1000); + final range1 = TimeRange(since: 100, until: 200); + final range2 = TimeRange(since: 150, until: 250); expect(range1.canMergeWith(range2), isTrue); expect(range2.canMergeWith(range1), isTrue); }); test('canMergeWith detects adjacent ranges', () { - final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); - final range2 = TimeRange(since: 201, until: 300, fetchedAt: 1000); + final range1 = TimeRange(since: 100, until: 200); + final range2 = TimeRange(since: 201, until: 300); expect(range1.canMergeWith(range2), isTrue); expect(range2.canMergeWith(range1), isTrue); }); test('canMergeWith returns false for non-adjacent ranges', () { - final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); - final range2 = TimeRange(since: 300, until: 400, fetchedAt: 1000); + final range1 = TimeRange(since: 100, until: 200); + final range2 = TimeRange(since: 300, until: 400); expect(range1.canMergeWith(range2), isFalse); expect(range2.canMergeWith(range1), isFalse); }); test('mergeWith combines ranges correctly', () { - final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); - final range2 = TimeRange(since: 150, until: 300, fetchedAt: 2000); + final range1 = TimeRange(since: 100, until: 200); + final range2 = TimeRange(since: 150, until: 300); final merged = range1.mergeWith(range2); expect(merged.since, 100); expect(merged.until, 300); - expect(merged.fetchedAt, 2000); // Takes the most recent fetchedAt }); test('contains checks if range fully contains a period', () { - final range = TimeRange(since: 100, until: 300, fetchedAt: 1000); + final range = TimeRange(since: 100, until: 300); expect(range.contains(150, 250), isTrue); expect(range.contains(100, 300), isTrue); @@ -60,7 +57,7 @@ void main() { }); test('overlaps checks if range overlaps with a period', () { - final range = TimeRange(since: 100, until: 300, fetchedAt: 1000); + final range = TimeRange(since: 100, until: 300); expect(range.overlaps(150, 250), isTrue); expect(range.overlaps(50, 150), isTrue); @@ -73,7 +70,6 @@ void main() { final range = TimeRange( since: 1704067200, until: 1704153600, - fetchedAt: 1704160000, ); final json = range.toJson(); @@ -81,13 +77,12 @@ void main() { expect(restored.since, range.since); expect(restored.until, range.until); - expect(restored.fetchedAt, range.fetchedAt); }); test('equality works correctly', () { - final range1 = TimeRange(since: 100, until: 200, fetchedAt: 1000); - final range2 = TimeRange(since: 100, until: 200, fetchedAt: 1000); - final range3 = TimeRange(since: 100, until: 300, fetchedAt: 1000); + final range1 = TimeRange(since: 100, until: 200); + final range2 = TimeRange(since: 100, until: 200); + final range3 = TimeRange(since: 100, until: 300); expect(range1, equals(range2)); expect(range1, isNot(equals(range3))); @@ -100,9 +95,9 @@ void main() { relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ - TimeRange(since: 200, until: 300, fetchedAt: 1000), - TimeRange(since: 100, until: 150, fetchedAt: 1000), - TimeRange(since: 500, until: 600, fetchedAt: 1000), + TimeRange(since: 200, until: 300), + TimeRange(since: 100, until: 150), + TimeRange(since: 500, until: 600), ], ); @@ -121,19 +116,21 @@ void main() { expect(coverage.newest, isNull); }); - test('reachedOldest getter works correctly', () { + test('reachedOldest is true when oldest is 0', () { final coverageNotReached = RelayCoverage( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), - ranges: [], - reachedOldestAt: null, + ranges: [ + TimeRange(since: 100, until: 500), + ], ); final coverageReached = RelayCoverage( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), - ranges: [], - reachedOldestAt: 1704160000, + ranges: [ + TimeRange(since: 0, until: 500), + ], ); expect(coverageNotReached.reachedOldest, isFalse); @@ -159,7 +156,7 @@ void main() { relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ - TimeRange(since: 200, until: 300, fetchedAt: 1000), + TimeRange(since: 200, until: 300), ], ); @@ -180,8 +177,8 @@ void main() { relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ - TimeRange(since: 100, until: 150, fetchedAt: 1000), - TimeRange(since: 300, until: 400, fetchedAt: 1000), + TimeRange(since: 100, until: 150), + TimeRange(since: 300, until: 400), ], ); @@ -201,7 +198,7 @@ void main() { relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ - TimeRange(since: 100, until: 500, fetchedAt: 1000), + TimeRange(since: 100, until: 500), ], ); @@ -215,7 +212,7 @@ void main() { relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ - TimeRange(since: 200, until: 300, fetchedAt: 1000), + TimeRange(since: 200, until: 300), ], ); @@ -235,18 +232,12 @@ void main() { relayUrl: 'wss://relay.example.com', rangeStart: 1704067200, rangeEnd: 1704153600, - fetchedAt: 1704160000, - reachedOldest: true, - reachedOldestAt: 1704160000, ); expect(record.filterHash, 'abc123'); expect(record.relayUrl, 'wss://relay.example.com'); expect(record.rangeStart, 1704067200); expect(record.rangeEnd, 1704153600); - expect(record.fetchedAt, 1704160000); - expect(record.reachedOldest, isTrue); - expect(record.reachedOldestAt, 1704160000); }); test('key is generated correctly', () { @@ -255,7 +246,6 @@ void main() { relayUrl: 'wss://relay.example.com', rangeStart: 1704067200, rangeEnd: 1704153600, - fetchedAt: 1704160000, ); expect(record.key, 'abc123:wss://relay.example.com:1704067200'); @@ -267,9 +257,6 @@ void main() { relayUrl: 'wss://relay.example.com', rangeStart: 1704067200, rangeEnd: 1704153600, - fetchedAt: 1704160000, - reachedOldest: true, - reachedOldestAt: 1704155000, ); final json = record.toJson(); @@ -279,9 +266,6 @@ void main() { expect(restored.relayUrl, record.relayUrl); expect(restored.rangeStart, record.rangeStart); expect(restored.rangeEnd, record.rangeEnd); - expect(restored.fetchedAt, record.fetchedAt); - expect(restored.reachedOldest, record.reachedOldest); - expect(restored.reachedOldestAt, record.reachedOldestAt); }); }); diff --git a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart index 8598b10bd..ffe406fb5 100644 --- a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart +++ b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart @@ -136,7 +136,7 @@ void main() async { await ndk.destroy(); }); - test('no coverage recorded when no events received', + test('coverage recorded with filter bounds when no events received', timeout: const Timeout(Duration(seconds: 5)), () async { // Empty - no events Map textNotes = {}; @@ -160,6 +160,7 @@ void main() async { await ndk.relays.seedRelaysConnected; + // Filter with explicit bounds final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], @@ -173,9 +174,13 @@ void main() async { final coverage = await ndk.coverage.getForFilter(filter); - // No events = no coverage recorded - expect(coverage.isEmpty, isTrue, - reason: 'No coverage should be recorded when no events received'); + // No events but filter has bounds = coverage recorded with filter bounds + expect(coverage.containsKey(relay1.url), isTrue, + reason: 'Coverage should be recorded using filter bounds'); + + final relayCoverage = coverage[relay1.url]!; + expect(relayCoverage.ranges.first.since, equals(100)); + expect(relayCoverage.ranges.first.until, equals(200)); await relay1.stopServer(); await ndk.destroy(); diff --git a/packages/ndk/test/usecases/coverage/coverage_test.dart b/packages/ndk/test/usecases/coverage/coverage_test.dart index 6606f5eab..d58d4fda0 100644 --- a/packages/ndk/test/usecases/coverage/coverage_test.dart +++ b/packages/ndk/test/usecases/coverage/coverage_test.dart @@ -259,26 +259,35 @@ void main() { }); }); - group('Coverage.markReachedOldest', () { - test('marks filter/relay as reached oldest', () async { + group('Coverage.reachedOldest', () { + test('reachedOldest is true when oldest is 0', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); await coverage.addRange( filter: filter, relayUrl: 'wss://relay.example.com', - since: 100, + since: 0, until: 200, ); - await coverage.markReachedOldest( + final result = await coverage.getForFilter(filter); + + expect(result['wss://relay.example.com']!.reachedOldest, isTrue); + }); + + test('reachedOldest is false when oldest is not 0', () async { + final filter = Filter(kinds: [1], authors: ['pubkey1']); + + await coverage.addRange( filter: filter, relayUrl: 'wss://relay.example.com', + since: 100, + until: 200, ); final result = await coverage.getForFilter(filter); - expect(result['wss://relay.example.com']!.reachedOldest, isTrue); - expect(result['wss://relay.example.com']!.reachedOldestAt, isNotNull); + expect(result['wss://relay.example.com']!.reachedOldest, isFalse); }); }); From adcd778abae7bcd2b9e79ad80afbaec694e95249 Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 7 Jan 2026 17:18:48 +0100 Subject: [PATCH 07/13] fix: timerange until --- .../usecases/requests/requests.dart | 5 +++-- .../coverage/coverage_integration_test.dart | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart index 94d658c18..4d61abe9a 100644 --- a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart +++ b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart @@ -307,10 +307,11 @@ class Requests { int until; if (relayEvents != null && relayEvents.isNotEmpty) { - // Use actual event timestamps + // Use oldest event timestamp for since, filter.until or now for until + // EOSE means relay has no more events, so coverage extends to query end final timestamps = relayEvents.map((e) => e.createdAt).toList(); since = timestamps.reduce((a, b) => a < b ? a : b); - until = timestamps.reduce((a, b) => a > b ? a : b); + until = filter.until ?? now; } else if (filter.since != null || filter.until != null) { // No events but filter has explicit bounds since = filter.since ?? 0; diff --git a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart index ffe406fb5..ddc0ee165 100644 --- a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart +++ b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart @@ -74,11 +74,12 @@ void main() async { expect(relayCoverage.ranges.isNotEmpty, isTrue, reason: 'Should have at least one range'); - // Coverage should be based on the event's createdAt (150) + // Coverage since should be based on the event's createdAt (150) + // Coverage until should extend to now (no filter.until specified) expect(relayCoverage.ranges.first.since, equals(150), - reason: 'Range since should match event timestamp'); - expect(relayCoverage.ranges.first.until, equals(150), - reason: 'Range until should match event timestamp (single event)'); + reason: 'Range since should match oldest event timestamp'); + expect(relayCoverage.ranges.first.until, greaterThan(150), + reason: 'Range until should extend to now (EOSE means no newer events)'); await relay1.stopServer(); await ndk.destroy(); @@ -126,11 +127,12 @@ void main() async { expect(coverage.containsKey(relay1.url), isTrue); final relayCoverage = coverage[relay1.url]!; - // Coverage should be 500-500 (the actual event), NOT 100-1000 + // Coverage since should be event timestamp (500), not filter.since (100) + // Coverage until should be filter.until (1000) since EOSE confirms no newer events expect(relayCoverage.ranges.first.since, equals(500), - reason: 'Coverage should reflect event timestamp, not filter since'); - expect(relayCoverage.ranges.first.until, equals(500), - reason: 'Coverage should reflect event timestamp, not filter until'); + reason: 'Coverage since should reflect oldest event timestamp'); + expect(relayCoverage.ranges.first.until, equals(1000), + reason: 'Coverage until should be filter.until (EOSE confirms coverage)'); await relay1.stopServer(); await ndk.destroy(); @@ -224,9 +226,9 @@ void main() async { expect(coverage.containsKey(relay1.url), isTrue); final relayCoverage = coverage[relay1.url]!; - // With single event at timestamp 100 + // since = oldest event timestamp, until = now (no filter.until) expect(relayCoverage.ranges.first.since, equals(100)); - expect(relayCoverage.ranges.first.until, equals(100)); + expect(relayCoverage.ranges.first.until, greaterThan(100)); await relay1.stopServer(); await ndk.destroy(); From daf0080fa75e4e7c6b93ec759b322d3f22aa7330 Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 14 Jan 2026 13:21:02 +0100 Subject: [PATCH 08/13] feat: coverage disabled by default --- packages/ndk/lib/presentation_layer/init.dart | 6 ++++-- packages/ndk/lib/presentation_layer/ndk_config.dart | 6 ++++++ .../test/usecases/coverage/coverage_integration_test.dart | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ndk/lib/presentation_layer/init.dart b/packages/ndk/lib/presentation_layer/init.dart index b59567478..001b6dc2b 100644 --- a/packages/ndk/lib/presentation_layer/init.dart +++ b/packages/ndk/lib/presentation_layer/init.dart @@ -245,8 +245,10 @@ class Initialization { cacheManager: _ndkConfig.cache, ); - // Connect coverage to requests for automatic range recording - requests.coverage = coverage; + // Connect coverage to requests for automatic range recording (if enabled) + if (_ndkConfig.coverageEnabled) { + requests.coverage = coverage; + } giftWrap = GiftWrap(accounts: accounts); diff --git a/packages/ndk/lib/presentation_layer/ndk_config.dart b/packages/ndk/lib/presentation_layer/ndk_config.dart index e0e08431e..816950b66 100644 --- a/packages/ndk/lib/presentation_layer/ndk_config.dart +++ b/packages/ndk/lib/presentation_layer/ndk_config.dart @@ -53,6 +53,11 @@ class NdkConfig { /// User agent string for Http requests and websockets. String userAgent; + /// Enable filter coverage tracking. + /// When enabled, NDK tracks which time ranges have been fetched from which relays. + /// Disabled by default for performance. + bool coverageEnabled; + /// Creates a new instance of [NdkConfig]. /// /// [eventVerifier] The verifier used to validate Nostr events. \ @@ -76,6 +81,7 @@ class NdkConfig { BroadcastDefaults.CONSIDER_DONE_PERCENT, this.logLevel = defaultLogLevel, this.userAgent = RequestDefaults.DEFAULT_USER_AGENT, + this.coverageEnabled = false, }); } diff --git a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart index ddc0ee165..421961133 100644 --- a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart +++ b/packages/ndk/test/usecases/coverage/coverage_integration_test.dart @@ -43,6 +43,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], + coverageEnabled: true, ), ); @@ -105,6 +106,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], + coverageEnabled: true, ), ); @@ -157,6 +159,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], + coverageEnabled: true, ), ); @@ -207,6 +210,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], + coverageEnabled: true, ), ); From e90eec550e49ad744a7b50f9a6f2ea1b37cf524f Mon Sep 17 00:00:00 2001 From: Gringo Date: Wed, 14 Jan 2026 13:24:06 +0100 Subject: [PATCH 09/13] feat: mark as experimental --- packages/ndk/lib/presentation_layer/ndk.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ndk/lib/presentation_layer/ndk.dart b/packages/ndk/lib/presentation_layer/ndk.dart index eca124e9c..9c2acebcf 100644 --- a/packages/ndk/lib/presentation_layer/ndk.dart +++ b/packages/ndk/lib/presentation_layer/ndk.dart @@ -150,6 +150,7 @@ class Ndk { /// Filter coverage tracking /// Track which time ranges have been fetched from which relays for each filter + @experimental Coverage get coverage => _initialization.coverage; /// Close all transports on relay manager From aed731fb3cc2f89fb4b4365dd8cfd1a21701dbe5 Mon Sep 17 00:00:00 2001 From: Gringo Date: Fri, 16 Jan 2026 11:39:04 +0100 Subject: [PATCH 10/13] fix: rename coverage usecase to fetchedRanges --- .../cache_manager/mem_cache_manager.dart | 49 +++--- ...verage.dart => filter_fetched_ranges.dart} | 34 ++-- .../repositories/cache_manager.dart | 41 ++--- .../fetched_ranges.dart} | 94 +++++------ .../usecases/requests/requests.dart | 25 +-- packages/ndk/lib/ndk.dart | 4 +- packages/ndk/lib/presentation_layer/init.dart | 12 +- packages/ndk/lib/presentation_layer/ndk.dart | 6 +- .../lib/presentation_layer/ndk_config.dart | 6 +- ...t.dart => filter_fetched_ranges_test.dart} | 58 +++---- .../fetched_ranges_integration_test.dart} | 92 ++++++----- .../fetched_ranges_test.dart} | 154 +++++++++--------- 12 files changed, 291 insertions(+), 284 deletions(-) rename packages/ndk/lib/domain_layer/entities/{filter_coverage.dart => filter_fetched_ranges.dart} (86%) rename packages/ndk/lib/domain_layer/usecases/{coverage/coverage.dart => fetched_ranges/fetched_ranges.dart} (62%) rename packages/ndk/test/entities/{filter_coverage_test.dart => filter_fetched_ranges_test.dart} (86%) rename packages/ndk/test/usecases/{coverage/coverage_integration_test.dart => fetched_ranges/fetched_ranges_integration_test.dart} (63%) rename packages/ndk/test/usecases/{coverage/coverage_test.dart => fetched_ranges/fetched_ranges_test.dart} (72%) diff --git a/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart b/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart index 0849d2418..4fbe9e434 100644 --- a/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart +++ b/packages/ndk/lib/data_layer/repositories/cache_manager/mem_cache_manager.dart @@ -1,7 +1,7 @@ import 'dart:core'; import '../../../domain_layer/entities/contact_list.dart'; -import '../../../domain_layer/entities/filter_coverage.dart'; +import '../../../domain_layer/entities/filter_fetched_ranges.dart'; import '../../../domain_layer/entities/nip_01_event.dart'; import '../../../domain_layer/entities/nip_05.dart'; import '../../../domain_layer/entities/relay_set.dart'; @@ -31,9 +31,9 @@ class MemCacheManager implements CacheManager { /// In memory storage Map events = {}; - /// In memory storage for filter coverage records + /// In memory storage for filter fetched range records /// Key is filterHash:relayUrl:rangeStart - Map filterCoverageRecords = {}; + Map filterFetchedRangeRecords = {}; @override Future saveUserRelayList(UserRelayList userRelayList) async { @@ -304,67 +304,68 @@ class MemCacheManager implements CacheManager { } // ===================== - // Filter Coverage + // Filter Fetched Ranges // ===================== @override - Future saveFilterCoverageRecord(FilterCoverageRecord record) async { - filterCoverageRecords[record.key] = record; + Future saveFilterFetchedRangeRecord( + FilterFetchedRangeRecord record) async { + filterFetchedRangeRecords[record.key] = record; } @override - Future saveFilterCoverageRecords( - List records) async { + Future saveFilterFetchedRangeRecords( + List records) async { for (final record in records) { - filterCoverageRecords[record.key] = record; + filterFetchedRangeRecords[record.key] = record; } } @override - Future> loadFilterCoverageRecords( + Future> loadFilterFetchedRangeRecords( String filterHash) async { - return filterCoverageRecords.values + return filterFetchedRangeRecords.values .where((r) => r.filterHash == filterHash) .toList(); } @override - Future> loadFilterCoverageRecordsByRelay( + Future> loadFilterFetchedRangeRecordsByRelay( String filterHash, String relayUrl) async { - return filterCoverageRecords.values + return filterFetchedRangeRecords.values .where((r) => r.filterHash == filterHash && r.relayUrl == relayUrl) .toList(); } @override - Future> loadFilterCoverageRecordsByRelayUrl( - String relayUrl) async { - return filterCoverageRecords.values + Future> + loadFilterFetchedRangeRecordsByRelayUrl(String relayUrl) async { + return filterFetchedRangeRecords.values .where((r) => r.relayUrl == relayUrl) .toList(); } @override - Future removeFilterCoverageRecords(String filterHash) async { - filterCoverageRecords + Future removeFilterFetchedRangeRecords(String filterHash) async { + filterFetchedRangeRecords .removeWhere((key, value) => value.filterHash == filterHash); } @override - Future removeFilterCoverageRecordsByFilterAndRelay( + Future removeFilterFetchedRangeRecordsByFilterAndRelay( String filterHash, String relayUrl) async { - filterCoverageRecords.removeWhere((key, value) => + filterFetchedRangeRecords.removeWhere((key, value) => value.filterHash == filterHash && value.relayUrl == relayUrl); } @override - Future removeFilterCoverageRecordsByRelay(String relayUrl) async { - filterCoverageRecords + Future removeFilterFetchedRangeRecordsByRelay(String relayUrl) async { + filterFetchedRangeRecords .removeWhere((key, value) => value.relayUrl == relayUrl); } @override - Future removeAllFilterCoverageRecords() async { - filterCoverageRecords.clear(); + Future removeAllFilterFetchedRangeRecords() async { + filterFetchedRangeRecords.clear(); } } diff --git a/packages/ndk/lib/domain_layer/entities/filter_coverage.dart b/packages/ndk/lib/domain_layer/entities/filter_fetched_ranges.dart similarity index 86% rename from packages/ndk/lib/domain_layer/entities/filter_coverage.dart rename to packages/ndk/lib/domain_layer/entities/filter_fetched_ranges.dart index c60727040..f465739ee 100644 --- a/packages/ndk/lib/domain_layer/entities/filter_coverage.dart +++ b/packages/ndk/lib/domain_layer/entities/filter_fetched_ranges.dart @@ -63,29 +63,29 @@ class TimeRange { ); } -/// A gap in coverage (missing time range) -class CoverageGap { +/// A gap in fetched ranges (missing time range) +class FetchedRangesGap { final String relayUrl; final int since; final int until; - const CoverageGap({ + const FetchedRangesGap({ required this.relayUrl, required this.since, required this.until, }); @override - String toString() => 'CoverageGap($relayUrl: $since - $until)'; + String toString() => 'FetchedRangesGap($relayUrl: $since - $until)'; } -/// Coverage information for a specific relay and filter combination -class RelayCoverage { +/// Fetched ranges information for a specific relay and filter combination +class RelayFetchedRanges { final String relayUrl; final Filter filter; final List ranges; - const RelayCoverage({ + const RelayFetchedRanges({ required this.relayUrl, required this.filter, required this.ranges, @@ -148,10 +148,10 @@ class RelayCoverage { return gaps; } - /// Get gaps as CoverageGap objects - List getGaps(int since, int until) { + /// Get gaps as FetchedRangesGap objects + List getGaps(int since, int until) { return findGaps(since, until) - .map((g) => CoverageGap( + .map((g) => FetchedRangesGap( relayUrl: relayUrl, since: g.since, until: g.until, @@ -161,11 +161,11 @@ class RelayCoverage { @override String toString() => - 'RelayCoverage($relayUrl, ${ranges.length} ranges, oldest: $oldest, newest: $newest, reachedOldest: $reachedOldest)'; + 'RelayFetchedRanges($relayUrl, ${ranges.length} ranges, oldest: $oldest, newest: $newest, reachedOldest: $reachedOldest)'; } -/// Record stored in the database for filter coverage -class FilterCoverageRecord { +/// Record stored in the database for filter fetched ranges +class FilterFetchedRangeRecord { /// Hash of the filter (without since/until) final String filterHash; @@ -178,7 +178,7 @@ class FilterCoverageRecord { /// End of the covered range final int rangeEnd; - const FilterCoverageRecord({ + const FilterFetchedRangeRecord({ required this.filterHash, required this.relayUrl, required this.rangeStart, @@ -190,7 +190,7 @@ class FilterCoverageRecord { @override String toString() => - 'FilterCoverageRecord($filterHash, $relayUrl, $rangeStart-$rangeEnd)'; + 'FilterFetchedRangeRecord($filterHash, $relayUrl, $rangeStart-$rangeEnd)'; Map toJson() => { 'filterHash': filterHash, @@ -199,8 +199,8 @@ class FilterCoverageRecord { 'rangeEnd': rangeEnd, }; - factory FilterCoverageRecord.fromJson(Map json) => - FilterCoverageRecord( + factory FilterFetchedRangeRecord.fromJson(Map json) => + FilterFetchedRangeRecord( filterHash: json['filterHash'] as String, relayUrl: json['relayUrl'] as String, rangeStart: json['rangeStart'] as int, diff --git a/packages/ndk/lib/domain_layer/repositories/cache_manager.dart b/packages/ndk/lib/domain_layer/repositories/cache_manager.dart index d79084412..7a3eefdf0 100644 --- a/packages/ndk/lib/domain_layer/repositories/cache_manager.dart +++ b/packages/ndk/lib/domain_layer/repositories/cache_manager.dart @@ -1,5 +1,5 @@ import '../entities/contact_list.dart'; -import '../entities/filter_coverage.dart'; +import '../entities/filter_fetched_ranges.dart'; import '../entities/nip_01_event.dart'; import '../entities/nip_05.dart'; import '../entities/relay_set.dart'; @@ -82,37 +82,38 @@ abstract class CacheManager { Future removeAllNip05s(); // ===================== - // Filter Coverage + // Filter Fetched Ranges // ===================== - /// Save a filter coverage record - Future saveFilterCoverageRecord(FilterCoverageRecord record); + /// Save a filter fetched range record + Future saveFilterFetchedRangeRecord(FilterFetchedRangeRecord record); - /// Save multiple filter coverage records - Future saveFilterCoverageRecords(List records); + /// Save multiple filter fetched range records + Future saveFilterFetchedRangeRecords( + List records); - /// Load all coverage records for a filter hash - Future> loadFilterCoverageRecords( + /// Load all fetched range records for a filter hash + Future> loadFilterFetchedRangeRecords( String filterHash); - /// Load all coverage records for a filter hash and relay - Future> loadFilterCoverageRecordsByRelay( + /// Load all fetched range records for a filter hash and relay + Future> loadFilterFetchedRangeRecordsByRelay( String filterHash, String relayUrl); - /// Load all coverage records for a relay (all filters) - Future> loadFilterCoverageRecordsByRelayUrl( + /// Load all fetched range records for a relay (all filters) + Future> loadFilterFetchedRangeRecordsByRelayUrl( String relayUrl); - /// Remove all coverage records for a filter hash - Future removeFilterCoverageRecords(String filterHash); + /// Remove all fetched range records for a filter hash + Future removeFilterFetchedRangeRecords(String filterHash); - /// Remove coverage records for a specific filter hash and relay - Future removeFilterCoverageRecordsByFilterAndRelay( + /// Remove fetched range records for a specific filter hash and relay + Future removeFilterFetchedRangeRecordsByFilterAndRelay( String filterHash, String relayUrl); - /// Remove all coverage records for a relay - Future removeFilterCoverageRecordsByRelay(String relayUrl); + /// Remove all fetched range records for a relay + Future removeFilterFetchedRangeRecordsByRelay(String relayUrl); - /// Remove all filter coverage records - Future removeAllFilterCoverageRecords(); + /// Remove all filter fetched range records + Future removeAllFilterFetchedRangeRecords(); } diff --git a/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart b/packages/ndk/lib/domain_layer/usecases/fetched_ranges/fetched_ranges.dart similarity index 62% rename from packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart rename to packages/ndk/lib/domain_layer/usecases/fetched_ranges/fetched_ranges.dart index b2b83272c..22d25c9d1 100644 --- a/packages/ndk/lib/domain_layer/usecases/coverage/coverage.dart +++ b/packages/ndk/lib/domain_layer/usecases/fetched_ranges/fetched_ranges.dart @@ -1,43 +1,44 @@ import '../../entities/filter.dart'; -import '../../entities/filter_coverage.dart'; +import '../../entities/filter_fetched_ranges.dart'; import '../../repositories/cache_manager.dart'; -/// Usecase to track and query filter coverage per relay -class Coverage { +/// Usecase to track and query fetched ranges per relay +class FetchedRanges { final CacheManager _cacheManager; - Coverage({ + FetchedRanges({ required CacheManager cacheManager, }) : _cacheManager = cacheManager; - /// Get coverage for a filter across all relays - Future> getForFilter(Filter filter) async { + /// Get fetched ranges for a filter across all relays + Future> getForFilter(Filter filter) async { final filterHash = FilterFingerprint.generate(filter); - final records = await _cacheManager.loadFilterCoverageRecords(filterHash); + final records = + await _cacheManager.loadFilterFetchedRangeRecords(filterHash); - return _buildCoverageMap(filter, records); + return _buildFetchedRangesMap(filter, records); } - /// Get all coverages for a relay (all filters) - Future> getForRelay(String relayUrl) async { + /// Get all fetched ranges for a relay (all filters) + Future> getForRelay(String relayUrl) async { final records = - await _cacheManager.loadFilterCoverageRecordsByRelayUrl(relayUrl); + await _cacheManager.loadFilterFetchedRangeRecordsByRelayUrl(relayUrl); // Group by filterHash - final grouped = >{}; + final grouped = >{}; for (final record in records) { grouped.putIfAbsent(record.filterHash, () => []).add(record); } - // Build RelayCoverage for each filter + // Build RelayFetchedRanges for each filter // Note: We don't have the original filter, so we create an empty one // The filterHash is preserved but the filter details are not available - final result = []; + final result = []; for (final entry in grouped.entries) { final relayRecords = entry.value.where((r) => r.relayUrl == relayUrl).toList(); if (relayRecords.isNotEmpty) { - result.add(_buildRelayCoverage( + result.add(_buildRelayFetchedRanges( relayUrl, Filter(), // Empty filter - we only have the hash relayRecords, @@ -48,16 +49,16 @@ class Coverage { return result; } - /// Find gaps in coverage for a filter within a time range - Future> findGaps({ + /// Find gaps in fetched ranges for a filter within a time range + Future> findGaps({ required Filter filter, required int since, required int until, }) async { - final coverageMap = await getForFilter(filter); - final gaps = []; + final fetchedRangesMap = await getForFilter(filter); + final gaps = []; - for (final entry in coverageMap.entries) { + for (final entry in fetchedRangesMap.entries) { gaps.addAll(entry.value.getGaps(since, until)); } @@ -71,14 +72,14 @@ class Coverage { required int since, required int until, }) async { - final coverageMap = await getForFilter(filter); + final fetchedRangesMap = await getForFilter(filter); final result = >{}; - for (final entry in coverageMap.entries) { + for (final entry in fetchedRangesMap.entries) { final relayUrl = entry.key; - final coverage = entry.value; + final fetchedRanges = entry.value; - final gaps = coverage.findGaps(since, until); + final gaps = fetchedRanges.findGaps(since, until); if (gaps.isNotEmpty) { result[relayUrl] = gaps.map((gap) { final gapFilter = filter.clone(); @@ -92,7 +93,7 @@ class Coverage { return result; } - /// Add a coverage range for a filter/relay combination + /// Add a fetched range for a filter/relay combination /// Automatically merges with existing adjacent/overlapping ranges Future addRange({ required Filter filter, @@ -104,7 +105,7 @@ class Coverage { // Load existing records for this filter/relay final existingRecords = await _cacheManager - .loadFilterCoverageRecordsByRelay(filterHash, relayUrl); + .loadFilterFetchedRangeRecordsByRelay(filterHash, relayUrl); // Convert to TimeRanges for merging final ranges = existingRecords @@ -121,11 +122,11 @@ class Coverage { final mergedRanges = _mergeRanges(ranges); // Delete old records for this filter/relay and save merged ones - await _cacheManager.removeFilterCoverageRecordsByFilterAndRelay( + await _cacheManager.removeFilterFetchedRangeRecordsByFilterAndRelay( filterHash, relayUrl); final newRecords = mergedRanges.map((range) { - return FilterCoverageRecord( + return FilterFetchedRangeRecord( filterHash: filterHash, relayUrl: relayUrl, rangeStart: range.since, @@ -133,54 +134,55 @@ class Coverage { ); }).toList(); - await _cacheManager.saveFilterCoverageRecords(newRecords); + await _cacheManager.saveFilterFetchedRangeRecords(newRecords); } - /// Clear coverage for a specific filter + /// Clear fetched ranges for a specific filter Future clearForFilter(Filter filter) async { final filterHash = FilterFingerprint.generate(filter); - await _cacheManager.removeFilterCoverageRecords(filterHash); + await _cacheManager.removeFilterFetchedRangeRecords(filterHash); } - /// Clear all coverage records for a relay + /// Clear all fetched range records for a relay Future clearForRelay(String relayUrl) async { - await _cacheManager.removeFilterCoverageRecordsByRelay(relayUrl); + await _cacheManager.removeFilterFetchedRangeRecordsByRelay(relayUrl); } - /// Clear all coverage records + /// Clear all fetched range records Future clearAll() async { - await _cacheManager.removeAllFilterCoverageRecords(); + await _cacheManager.removeAllFilterFetchedRangeRecords(); } // ===================== // Private helpers // ===================== - /// Build a map of relay URL to RelayCoverage from records - Map _buildCoverageMap( + /// Build a map of relay URL to RelayFetchedRanges from records + Map _buildFetchedRangesMap( Filter filter, - List records, + List records, ) { // Group by relay - final grouped = >{}; + final grouped = >{}; for (final record in records) { grouped.putIfAbsent(record.relayUrl, () => []).add(record); } - // Build RelayCoverage for each relay - final result = {}; + // Build RelayFetchedRanges for each relay + final result = {}; for (final entry in grouped.entries) { - result[entry.key] = _buildRelayCoverage(entry.key, filter, entry.value); + result[entry.key] = + _buildRelayFetchedRanges(entry.key, filter, entry.value); } return result; } - /// Build a RelayCoverage from records for a single relay - RelayCoverage _buildRelayCoverage( + /// Build a RelayFetchedRanges from records for a single relay + RelayFetchedRanges _buildRelayFetchedRanges( String relayUrl, Filter filter, - List records, + List records, ) { final ranges = records .map((r) => TimeRange( @@ -190,7 +192,7 @@ class Coverage { .toList() ..sort((a, b) => a.since.compareTo(b.since)); - return RelayCoverage( + return RelayFetchedRanges( relayUrl: relayUrl, filter: filter, ranges: ranges, diff --git a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart index 4d61abe9a..6ebfda40d 100644 --- a/packages/ndk/lib/domain_layer/usecases/requests/requests.dart +++ b/packages/ndk/lib/domain_layer/usecases/requests/requests.dart @@ -15,7 +15,7 @@ import '../../entities/request_state.dart'; import '../../repositories/event_verifier.dart'; import '../cache_read/cache_read.dart'; import '../cache_write/cache_write.dart'; -import '../coverage/coverage.dart'; +import '../fetched_ranges/fetched_ranges.dart'; import '../engines/network_engine.dart'; import '../relay_manager.dart'; import '../stream_response_cleaner/stream_response_cleaner.dart'; @@ -32,7 +32,7 @@ class Requests { final EventVerifier _eventVerifier; final List _eventOutFilters; final Duration _defaultQueryTimeout; - Coverage? _coverage; + FetchedRanges? _fetchedRanges; /// Creates a new [Requests] instance /// @@ -59,8 +59,9 @@ class Requests { _eventOutFilters = eventOutFilters, _defaultQueryTimeout = defaultQueryTimeout; - /// Set the coverage tracker for automatic range recording - set coverage(Coverage? coverage) => _coverage = coverage; + /// Set the fetched ranges tracker for automatic range recording + set fetchedRanges(FetchedRanges? fetchedRanges) => + _fetchedRanges = fetchedRanges; /// Performs a low-level Nostr query /// @@ -226,9 +227,9 @@ class Requests { eventOutFilters: _eventOutFilters, )(); - // Record coverage when network requests complete (EOSE received) + // Record fetched ranges when network requests complete (EOSE received) state.networkController.done.then((_) { - _recordCoverage(state); + _recordFetchedRanges(state); }); // cleanup on close @@ -273,12 +274,12 @@ class Requests { return response; } - /// Records coverage for each relay that received EOSE + /// Records fetched ranges for each relay that received EOSE /// - If events received: use min/max of event timestamps /// - If no events + filter has since/until: use filter bounds /// - If no events + no bounds: use 0 to now - void _recordCoverage(RequestState state) { - if (_coverage == null) return; + void _recordFetchedRanges(RequestState state) { + if (_fetchedRanges == null) return; final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; @@ -301,14 +302,14 @@ class Requests { final relayEvents = eventsByRelay[relayUrl]; - // Record coverage for each filter sent to this relay + // Record fetched range for each filter sent to this relay for (final filter in relayState.filters) { int since; int until; if (relayEvents != null && relayEvents.isNotEmpty) { // Use oldest event timestamp for since, filter.until or now for until - // EOSE means relay has no more events, so coverage extends to query end + // EOSE means relay has no more events, so fetched range extends to query end final timestamps = relayEvents.map((e) => e.createdAt).toList(); since = timestamps.reduce((a, b) => a < b ? a : b); until = filter.until ?? now; @@ -322,7 +323,7 @@ class Requests { until = now; } - _coverage!.addRange( + _fetchedRanges!.addRange( filter: filter, relayUrl: relayUrl, since: since, diff --git a/packages/ndk/lib/ndk.dart b/packages/ndk/lib/ndk.dart index 3e126ba90..6f54281dd 100644 --- a/packages/ndk/lib/ndk.dart +++ b/packages/ndk/lib/ndk.dart @@ -81,8 +81,8 @@ export 'domain_layer/usecases/gift_wrap/gift_wrap.dart'; export 'domain_layer/usecases/bunkers/bunkers.dart'; export 'domain_layer/usecases/bunkers/models/bunker_connection.dart'; export 'domain_layer/usecases/bunkers/models/nostr_connect.dart'; -export 'domain_layer/usecases/coverage/coverage.dart'; -export 'domain_layer/entities/filter_coverage.dart'; +export 'domain_layer/usecases/fetched_ranges/fetched_ranges.dart'; +export 'domain_layer/entities/filter_fetched_ranges.dart'; /** * other stuff diff --git a/packages/ndk/lib/presentation_layer/init.dart b/packages/ndk/lib/presentation_layer/init.dart index 001b6dc2b..d269d0f5c 100644 --- a/packages/ndk/lib/presentation_layer/init.dart +++ b/packages/ndk/lib/presentation_layer/init.dart @@ -15,7 +15,7 @@ import '../domain_layer/usecases/accounts/accounts.dart'; import '../domain_layer/usecases/broadcast/broadcast.dart'; import '../domain_layer/usecases/bunkers/bunkers.dart'; import '../domain_layer/usecases/cache_read/cache_read.dart'; -import '../domain_layer/usecases/coverage/coverage.dart'; +import '../domain_layer/usecases/fetched_ranges/fetched_ranges.dart'; import '../domain_layer/usecases/cache_write/cache_write.dart'; import '../domain_layer/usecases/connectivity/connectivity.dart'; import '../domain_layer/usecases/engines/network_engine.dart'; @@ -80,7 +80,7 @@ class Initialization { late Search search; late GiftWrap giftWrap; late Connectivy connectivity; - late Coverage coverage; + late FetchedRanges fetchedRanges; late VerifyNip05 verifyNip05; @@ -241,13 +241,13 @@ class Initialization { requests: requests, ); - coverage = Coverage( + fetchedRanges = FetchedRanges( cacheManager: _ndkConfig.cache, ); - // Connect coverage to requests for automatic range recording (if enabled) - if (_ndkConfig.coverageEnabled) { - requests.coverage = coverage; + // Connect fetchedRanges to requests for automatic range recording (if enabled) + if (_ndkConfig.fetchedRangesEnabled) { + requests.fetchedRanges = fetchedRanges; } giftWrap = GiftWrap(accounts: accounts); diff --git a/packages/ndk/lib/presentation_layer/ndk.dart b/packages/ndk/lib/presentation_layer/ndk.dart index 9c2acebcf..4a49bb40c 100644 --- a/packages/ndk/lib/presentation_layer/ndk.dart +++ b/packages/ndk/lib/presentation_layer/ndk.dart @@ -7,7 +7,7 @@ import '../domain_layer/usecases/accounts/accounts.dart'; import '../domain_layer/usecases/broadcast/broadcast.dart'; import '../domain_layer/usecases/bunkers/bunkers.dart'; import '../domain_layer/usecases/connectivity/connectivity.dart'; -import '../domain_layer/usecases/coverage/coverage.dart'; +import '../domain_layer/usecases/fetched_ranges/fetched_ranges.dart'; import '../domain_layer/usecases/files/blossom.dart'; import '../domain_layer/usecases/files/blossom_user_server_list.dart'; import '../domain_layer/usecases/files/files.dart'; @@ -148,10 +148,10 @@ class Ndk { @experimental Search get search => _initialization.search; - /// Filter coverage tracking + /// Fetched ranges tracking /// Track which time ranges have been fetched from which relays for each filter @experimental - Coverage get coverage => _initialization.coverage; + FetchedRanges get fetchedRanges => _initialization.fetchedRanges; /// Close all transports on relay manager Future destroy() async { diff --git a/packages/ndk/lib/presentation_layer/ndk_config.dart b/packages/ndk/lib/presentation_layer/ndk_config.dart index 816950b66..267d06f56 100644 --- a/packages/ndk/lib/presentation_layer/ndk_config.dart +++ b/packages/ndk/lib/presentation_layer/ndk_config.dart @@ -53,10 +53,10 @@ class NdkConfig { /// User agent string for Http requests and websockets. String userAgent; - /// Enable filter coverage tracking. + /// Enable fetched ranges tracking. /// When enabled, NDK tracks which time ranges have been fetched from which relays. /// Disabled by default for performance. - bool coverageEnabled; + bool fetchedRangesEnabled; /// Creates a new instance of [NdkConfig]. /// @@ -81,7 +81,7 @@ class NdkConfig { BroadcastDefaults.CONSIDER_DONE_PERCENT, this.logLevel = defaultLogLevel, this.userAgent = RequestDefaults.DEFAULT_USER_AGENT, - this.coverageEnabled = false, + this.fetchedRangesEnabled = false, }); } diff --git a/packages/ndk/test/entities/filter_coverage_test.dart b/packages/ndk/test/entities/filter_fetched_ranges_test.dart similarity index 86% rename from packages/ndk/test/entities/filter_coverage_test.dart rename to packages/ndk/test/entities/filter_fetched_ranges_test.dart index 72639b419..0070a5948 100644 --- a/packages/ndk/test/entities/filter_coverage_test.dart +++ b/packages/ndk/test/entities/filter_fetched_ranges_test.dart @@ -89,9 +89,9 @@ void main() { }); }); - group('RelayCoverage', () { + group('RelayFetchedRanges', () { test('oldest and newest return correct values', () { - final coverage = RelayCoverage( + final fetchedRanges = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ @@ -101,23 +101,23 @@ void main() { ], ); - expect(coverage.oldest, 100); - expect(coverage.newest, 600); + expect(fetchedRanges.oldest, 100); + expect(fetchedRanges.newest, 600); }); test('oldest and newest return null for empty ranges', () { - final coverage = RelayCoverage( + final fetchedRanges = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [], ); - expect(coverage.oldest, isNull); - expect(coverage.newest, isNull); + expect(fetchedRanges.oldest, isNull); + expect(fetchedRanges.newest, isNull); }); test('reachedOldest is true when oldest is 0', () { - final coverageNotReached = RelayCoverage( + final fetchedRangesNotReached = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ @@ -125,7 +125,7 @@ void main() { ], ); - final coverageReached = RelayCoverage( + final fetchedRangesReached = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ @@ -133,18 +133,18 @@ void main() { ], ); - expect(coverageNotReached.reachedOldest, isFalse); - expect(coverageReached.reachedOldest, isTrue); + expect(fetchedRangesNotReached.reachedOldest, isFalse); + expect(fetchedRangesReached.reachedOldest, isTrue); }); test('findGaps returns correct gaps for empty ranges', () { - final coverage = RelayCoverage( + final fetchedRanges = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [], ); - final gaps = coverage.findGaps(100, 500); + final gaps = fetchedRanges.findGaps(100, 500); expect(gaps.length, 1); expect(gaps[0].since, 100); @@ -152,7 +152,7 @@ void main() { }); test('findGaps returns correct gaps for single range', () { - final coverage = RelayCoverage( + final fetchedRanges = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ @@ -160,7 +160,7 @@ void main() { ], ); - final gaps = coverage.findGaps(100, 500); + final gaps = fetchedRanges.findGaps(100, 500); expect(gaps.length, 2); expect(gaps[0].since, 100); @@ -173,7 +173,7 @@ void main() { // Ranges: [100-150], [300-400] // Query: 0 to 600 // Expected gaps: [0-99], [151-299], [401-600] - final coverage = RelayCoverage( + final fetchedRanges = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ @@ -182,7 +182,7 @@ void main() { ], ); - final gaps = coverage.findGaps(0, 600); + final gaps = fetchedRanges.findGaps(0, 600); expect(gaps.length, 3); expect(gaps[0].since, 0); @@ -194,7 +194,7 @@ void main() { }); test('findGaps returns empty list when fully covered', () { - final coverage = RelayCoverage( + final fetchedRanges = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ @@ -202,13 +202,13 @@ void main() { ], ); - final gaps = coverage.findGaps(200, 400); + final gaps = fetchedRanges.findGaps(200, 400); expect(gaps, isEmpty); }); - test('getGaps returns CoverageGap objects', () { - final coverage = RelayCoverage( + test('getGaps returns FetchedRangesGap objects', () { + final fetchedRanges = RelayFetchedRanges( relayUrl: 'wss://relay.example.com', filter: Filter(kinds: [1]), ranges: [ @@ -216,7 +216,7 @@ void main() { ], ); - final gaps = coverage.getGaps(100, 500); + final gaps = fetchedRanges.getGaps(100, 500); expect(gaps.length, 2); expect(gaps[0].relayUrl, 'wss://relay.example.com'); @@ -225,9 +225,9 @@ void main() { }); }); - group('FilterCoverageRecord', () { + group('FilterFetchedRangeRecord', () { test('constructor initializes correctly', () { - final record = FilterCoverageRecord( + final record = FilterFetchedRangeRecord( filterHash: 'abc123', relayUrl: 'wss://relay.example.com', rangeStart: 1704067200, @@ -241,7 +241,7 @@ void main() { }); test('key is generated correctly', () { - final record = FilterCoverageRecord( + final record = FilterFetchedRangeRecord( filterHash: 'abc123', relayUrl: 'wss://relay.example.com', rangeStart: 1704067200, @@ -252,7 +252,7 @@ void main() { }); test('toJson and fromJson work correctly', () { - final record = FilterCoverageRecord( + final record = FilterFetchedRangeRecord( filterHash: 'abc123', relayUrl: 'wss://relay.example.com', rangeStart: 1704067200, @@ -260,7 +260,7 @@ void main() { ); final json = record.toJson(); - final restored = FilterCoverageRecord.fromJson(json); + final restored = FilterFetchedRangeRecord.fromJson(json); expect(restored.filterHash, record.filterHash); expect(restored.relayUrl, record.relayUrl); @@ -320,9 +320,9 @@ void main() { }); }); - group('CoverageGap', () { + group('FetchedRangesGap', () { test('constructor initializes correctly', () { - final gap = CoverageGap( + final gap = FetchedRangesGap( relayUrl: 'wss://relay.example.com', since: 100, until: 200, diff --git a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart similarity index 63% rename from packages/ndk/test/usecases/coverage/coverage_integration_test.dart rename to packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart index 421961133..165208616 100644 --- a/packages/ndk/test/usecases/coverage/coverage_integration_test.dart +++ b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart @@ -20,8 +20,8 @@ void main() async { return event; } - group('Coverage integration', () { - test('query automatically records coverage based on event timestamps', + group('FetchedRanges integration', () { + test('query automatically records fetched ranges based on event timestamps', timeout: const Timeout(Duration(seconds: 5)), () async { // Create event with specific timestamp final event1 = textNoteWithTimestamp(key1, 150); @@ -29,7 +29,7 @@ void main() async { // Setup mock relay MockRelay relay1 = MockRelay( - name: "relay coverage test", + name: "relay fetched ranges test", explicitPort: 4200, signEvents: false, ); @@ -43,7 +43,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], - coverageEnabled: true, + fetchedRangesEnabled: true, ), ); @@ -60,33 +60,34 @@ void main() async { // Wait for response to complete await response.future; - // Small delay to ensure coverage is recorded + // Small delay to ensure fetched ranges are recorded await Future.delayed(const Duration(milliseconds: 100)); - // Check coverage was recorded - final coverage = await ndk.coverage.getForFilter(filter); + // Check fetched ranges were recorded + final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); - expect(coverage.isNotEmpty, isTrue, - reason: 'Coverage should be recorded after query'); - expect(coverage.containsKey(relay1.url), isTrue, - reason: 'Coverage should contain the relay URL'); + expect(fetchedRanges.isNotEmpty, isTrue, + reason: 'Fetched ranges should be recorded after query'); + expect(fetchedRanges.containsKey(relay1.url), isTrue, + reason: 'Fetched ranges should contain the relay URL'); - final relayCoverage = coverage[relay1.url]!; - expect(relayCoverage.ranges.isNotEmpty, isTrue, + final relayFetchedRanges = fetchedRanges[relay1.url]!; + expect(relayFetchedRanges.ranges.isNotEmpty, isTrue, reason: 'Should have at least one range'); - // Coverage since should be based on the event's createdAt (150) - // Coverage until should extend to now (no filter.until specified) - expect(relayCoverage.ranges.first.since, equals(150), + // Fetched range since should be based on the event's createdAt (150) + // Fetched range until should extend to now (no filter.until specified) + expect(relayFetchedRanges.ranges.first.since, equals(150), reason: 'Range since should match oldest event timestamp'); - expect(relayCoverage.ranges.first.until, greaterThan(150), - reason: 'Range until should extend to now (EOSE means no newer events)'); + expect(relayFetchedRanges.ranges.first.until, greaterThan(150), + reason: + 'Range until should extend to now (EOSE means no newer events)'); await relay1.stopServer(); await ndk.destroy(); }); - test('coverage reflects actual events received, not filter bounds', + test('fetched ranges reflect actual events received, not filter bounds', timeout: const Timeout(Duration(seconds: 5)), () async { // Create event with timestamp 500 final event1 = textNoteWithTimestamp(key1, 500); @@ -106,7 +107,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], - coverageEnabled: true, + fetchedRangesEnabled: true, ), ); @@ -124,23 +125,24 @@ void main() async { await response.future; await Future.delayed(const Duration(milliseconds: 100)); - final coverage = await ndk.coverage.getForFilter(filter); + final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); - expect(coverage.containsKey(relay1.url), isTrue); + expect(fetchedRanges.containsKey(relay1.url), isTrue); - final relayCoverage = coverage[relay1.url]!; - // Coverage since should be event timestamp (500), not filter.since (100) - // Coverage until should be filter.until (1000) since EOSE confirms no newer events - expect(relayCoverage.ranges.first.since, equals(500), - reason: 'Coverage since should reflect oldest event timestamp'); - expect(relayCoverage.ranges.first.until, equals(1000), - reason: 'Coverage until should be filter.until (EOSE confirms coverage)'); + final relayFetchedRanges = fetchedRanges[relay1.url]!; + // Fetched range since should be event timestamp (500), not filter.since (100) + // Fetched range until should be filter.until (1000) since EOSE confirms no newer events + expect(relayFetchedRanges.ranges.first.since, equals(500), + reason: 'Fetched range since should reflect oldest event timestamp'); + expect(relayFetchedRanges.ranges.first.until, equals(1000), + reason: + 'Fetched range until should be filter.until (EOSE confirms coverage)'); await relay1.stopServer(); await ndk.destroy(); }); - test('coverage recorded with filter bounds when no events received', + test('fetched ranges recorded with filter bounds when no events received', timeout: const Timeout(Duration(seconds: 5)), () async { // Empty - no events Map textNotes = {}; @@ -159,7 +161,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], - coverageEnabled: true, + fetchedRangesEnabled: true, ), ); @@ -177,21 +179,21 @@ void main() async { await response.future; await Future.delayed(const Duration(milliseconds: 100)); - final coverage = await ndk.coverage.getForFilter(filter); + final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); - // No events but filter has bounds = coverage recorded with filter bounds - expect(coverage.containsKey(relay1.url), isTrue, - reason: 'Coverage should be recorded using filter bounds'); + // No events but filter has bounds = fetched ranges recorded with filter bounds + expect(fetchedRanges.containsKey(relay1.url), isTrue, + reason: 'Fetched ranges should be recorded using filter bounds'); - final relayCoverage = coverage[relay1.url]!; - expect(relayCoverage.ranges.first.since, equals(100)); - expect(relayCoverage.ranges.first.until, equals(200)); + final relayFetchedRanges = fetchedRanges[relay1.url]!; + expect(relayFetchedRanges.ranges.first.since, equals(100)); + expect(relayFetchedRanges.ranges.first.until, equals(200)); await relay1.stopServer(); await ndk.destroy(); }); - test('coverage uses event timestamp', + test('fetched ranges use event timestamp', timeout: const Timeout(Duration(seconds: 5)), () async { // Create event with specific timestamp final event1 = textNoteWithTimestamp(key1, 100); @@ -210,7 +212,7 @@ void main() async { cache: cache, engine: NdkEngine.RELAY_SETS, bootstrapRelays: [relay1.url], - coverageEnabled: true, + fetchedRangesEnabled: true, ), ); @@ -225,14 +227,14 @@ void main() async { await response.future; await Future.delayed(const Duration(milliseconds: 100)); - final coverage = await ndk.coverage.getForFilter(filter); + final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); - expect(coverage.containsKey(relay1.url), isTrue); + expect(fetchedRanges.containsKey(relay1.url), isTrue); - final relayCoverage = coverage[relay1.url]!; + final relayFetchedRanges = fetchedRanges[relay1.url]!; // since = oldest event timestamp, until = now (no filter.until) - expect(relayCoverage.ranges.first.since, equals(100)); - expect(relayCoverage.ranges.first.until, greaterThan(100)); + expect(relayFetchedRanges.ranges.first.since, equals(100)); + expect(relayFetchedRanges.ranges.first.until, greaterThan(100)); await relay1.stopServer(); await ndk.destroy(); diff --git a/packages/ndk/test/usecases/coverage/coverage_test.dart b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_test.dart similarity index 72% rename from packages/ndk/test/usecases/coverage/coverage_test.dart rename to packages/ndk/test/usecases/fetched_ranges/fetched_ranges_test.dart index d58d4fda0..72bd8a429 100644 --- a/packages/ndk/test/usecases/coverage/coverage_test.dart +++ b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_test.dart @@ -3,25 +3,25 @@ import 'package:ndk/ndk.dart'; void main() { late MemCacheManager cacheManager; - late Coverage coverage; + late FetchedRanges fetchedRanges; setUp(() { cacheManager = MemCacheManager(); - coverage = Coverage(cacheManager: cacheManager); + fetchedRanges = FetchedRanges(cacheManager: cacheManager); }); - group('Coverage.addRange', () { + group('FetchedRanges.addRange', () { test('adds a new range for a filter/relay', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result.containsKey('wss://relay.example.com'), isTrue); expect(result['wss://relay.example.com']!.ranges.length, 1); @@ -32,21 +32,21 @@ void main() { test('merges adjacent ranges', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 201, until: 300, ); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result['wss://relay.example.com']!.ranges.length, 1); expect(result['wss://relay.example.com']!.ranges[0].since, 100); @@ -56,21 +56,21 @@ void main() { test('merges overlapping ranges', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 100, until: 250, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 200, until: 400, ); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result['wss://relay.example.com']!.ranges.length, 1); expect(result['wss://relay.example.com']!.ranges[0].since, 100); @@ -80,21 +80,21 @@ void main() { test('keeps separate ranges when not adjacent', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 400, until: 500, ); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result['wss://relay.example.com']!.ranges.length, 2); }); @@ -102,21 +102,21 @@ void main() { test('tracks different relays separately', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay1.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay2.example.com', since: 150, until: 300, ); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result.length, 2); expect(result['wss://relay1.example.com']!.ranges[0].since, 100); @@ -129,33 +129,33 @@ void main() { final filter1 = Filter(kinds: [1], authors: ['pubkey1']); final filter2 = Filter(kinds: [0], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter1, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter2, relayUrl: 'wss://relay.example.com', since: 300, until: 400, ); - final result1 = await coverage.getForFilter(filter1); - final result2 = await coverage.getForFilter(filter2); + final result1 = await fetchedRanges.getForFilter(filter1); + final result2 = await fetchedRanges.getForFilter(filter2); expect(result1['wss://relay.example.com']!.ranges[0].since, 100); expect(result2['wss://relay.example.com']!.ranges[0].since, 300); }); }); - group('Coverage.findGaps', () { - test('returns full range as gap when no coverage', () async { + group('FetchedRanges.findGaps', () { + test('returns full range as gap when no fetched ranges', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - final gaps = await coverage.findGaps( + final gaps = await fetchedRanges.findGaps( filter: filter, since: 100, until: 500, @@ -167,14 +167,14 @@ void main() { test('returns gaps for partial coverage', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 200, until: 300, ); - final gaps = await coverage.findGaps( + final gaps = await fetchedRanges.findGaps( filter: filter, since: 100, until: 500, @@ -190,14 +190,14 @@ void main() { test('returns empty when fully covered', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 0, until: 1000, ); - final gaps = await coverage.findGaps( + final gaps = await fetchedRanges.findGaps( filter: filter, since: 100, until: 500, @@ -207,18 +207,18 @@ void main() { }); }); - group('Coverage.getOptimizedFilters', () { + group('FetchedRanges.getOptimizedFilters', () { test('returns filters for gaps only', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 200, until: 300, ); - final optimized = await coverage.getOptimizedFilters( + final optimized = await fetchedRanges.getOptimizedFilters( filter: filter, since: 100, until: 500, @@ -242,14 +242,14 @@ void main() { test('returns empty for fully covered relay', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 0, until: 1000, ); - final optimized = await coverage.getOptimizedFilters( + final optimized = await fetchedRanges.getOptimizedFilters( filter: filter, since: 100, until: 500, @@ -259,18 +259,18 @@ void main() { }); }); - group('Coverage.reachedOldest', () { + group('FetchedRanges.reachedOldest', () { test('reachedOldest is true when oldest is 0', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 0, until: 200, ); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result['wss://relay.example.com']!.reachedOldest, isTrue); }); @@ -278,157 +278,157 @@ void main() { test('reachedOldest is false when oldest is not 0', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result['wss://relay.example.com']!.reachedOldest, isFalse); }); }); - group('Coverage.getForRelay', () { - test('returns all coverages for a relay', () async { + group('FetchedRanges.getForRelay', () { + test('returns all fetched ranges for a relay', () async { final filter1 = Filter(kinds: [1], authors: ['pubkey1']); final filter2 = Filter(kinds: [0], authors: ['pubkey2']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter1, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter2, relayUrl: 'wss://relay.example.com', since: 300, until: 400, ); - final result = await coverage.getForRelay('wss://relay.example.com'); + final result = await fetchedRanges.getForRelay('wss://relay.example.com'); expect(result.length, 2); }); }); - group('Coverage.clearForFilter', () { - test('clears coverage for a specific filter', () async { + group('FetchedRanges.clearForFilter', () { + test('clears fetched ranges for a specific filter', () async { final filter1 = Filter(kinds: [1], authors: ['pubkey1']); final filter2 = Filter(kinds: [0], authors: ['pubkey2']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter1, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter2, relayUrl: 'wss://relay.example.com', since: 300, until: 400, ); - await coverage.clearForFilter(filter1); + await fetchedRanges.clearForFilter(filter1); - final result1 = await coverage.getForFilter(filter1); - final result2 = await coverage.getForFilter(filter2); + final result1 = await fetchedRanges.getForFilter(filter1); + final result2 = await fetchedRanges.getForFilter(filter2); expect(result1, isEmpty); expect(result2.isNotEmpty, isTrue); }); }); - group('Coverage.clearForRelay', () { - test('clears all coverage for a relay', () async { + group('FetchedRanges.clearForRelay', () { + test('clears all fetched ranges for a relay', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay1.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay2.example.com', since: 100, until: 200, ); - await coverage.clearForRelay('wss://relay1.example.com'); + await fetchedRanges.clearForRelay('wss://relay1.example.com'); - final result = await coverage.getForFilter(filter); + final result = await fetchedRanges.getForFilter(filter); expect(result.containsKey('wss://relay1.example.com'), isFalse); expect(result.containsKey('wss://relay2.example.com'), isTrue); }); }); - group('Coverage.clearAll', () { - test('clears all coverage', () async { + group('FetchedRanges.clearAll', () { + test('clears all fetched ranges', () async { final filter1 = Filter(kinds: [1], authors: ['pubkey1']); final filter2 = Filter(kinds: [0], authors: ['pubkey2']); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter1, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter2, relayUrl: 'wss://relay.example.com', since: 300, until: 400, ); - await coverage.clearAll(); + await fetchedRanges.clearAll(); - final result1 = await coverage.getForFilter(filter1); - final result2 = await coverage.getForFilter(filter2); + final result1 = await fetchedRanges.getForFilter(filter1); + final result2 = await fetchedRanges.getForFilter(filter2); expect(result1, isEmpty); expect(result2, isEmpty); }); }); - group('Coverage realistic scenarios', () { + group('FetchedRanges realistic scenarios', () { test('handles Jan-Mar + Jun-Sep scenario with Apr-May gap', () async { final filter = Filter(kinds: [1], authors: ['pubkey1']); - // Add Jan-Mar coverage - await coverage.addRange( + // Add Jan-Mar fetched range + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.damus.io', since: 1704067200, // Jan 1 until: 1711929599, // Mar 31 ); - // Add Jun-Sep coverage - await coverage.addRange( + // Add Jun-Sep fetched range + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.damus.io', since: 1717200000, // Jun 1 until: 1727740799, // Sep 30 ); - final result = await coverage.getForFilter(filter); - final damusCoverage = result['wss://relay.damus.io']!; + final result = await fetchedRanges.getForFilter(filter); + final damusFetchedRanges = result['wss://relay.damus.io']!; // Should have 2 separate ranges - expect(damusCoverage.ranges.length, 2); + expect(damusFetchedRanges.ranges.length, 2); // Check gaps for Jan-Oct query - final gaps = await coverage.findGaps( + final gaps = await fetchedRanges.findGaps( filter: filter, since: 1704067200, // Jan 1 until: 1730419199, // Oct 31 @@ -442,14 +442,14 @@ void main() { final filter = Filter(kinds: [1], authors: ['pubkey1']); // Initial: two separate ranges - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 100, until: 200, ); - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 400, @@ -457,11 +457,11 @@ void main() { ); // Verify 2 ranges and a gap - var result = await coverage.getForFilter(filter); + var result = await fetchedRanges.getForFilter(filter); expect(result['wss://relay.example.com']!.ranges.length, 2); // Fill the gap - await coverage.addRange( + await fetchedRanges.addRange( filter: filter, relayUrl: 'wss://relay.example.com', since: 201, @@ -469,7 +469,7 @@ void main() { ); // Should now be 1 merged range - result = await coverage.getForFilter(filter); + result = await fetchedRanges.getForFilter(filter); expect(result['wss://relay.example.com']!.ranges.length, 1); expect(result['wss://relay.example.com']!.ranges[0].since, 100); expect(result['wss://relay.example.com']!.ranges[0].until, 500); From 5d802073de22ba90c0ad61c0efd0ab9453aea860 Mon Sep 17 00:00:00 2001 From: Gringo Date: Fri, 16 Jan 2026 12:04:21 +0100 Subject: [PATCH 11/13] feat: cache managers support --- .../lib/data_layer/data_sources/isar_db.dart | 4 +- .../lib/data_layer/models/db/db_event.g.dart | 2169 ----------------- .../db/db_filter_fetched_range_record.dart | 46 + .../db/db_filter_fetched_range_record.g.dart | 1314 ++++++++++ .../data_layer/models/db/db_metadata.g.dart | 322 ++- .../data_layer/models/db/db_relay_set.g.dart | 18 +- .../cache_manager/isar_cache_manager.dart | 110 + .../fetched_ranges_integration_test.dart | 12 +- .../db/object_box/db_object_box.dart | 108 + .../db_filter_fetched_range_record.dart | 51 + packages/objectbox/lib/objectbox-model.json | 42 +- packages/objectbox/lib/objectbox.g.dart | 135 +- .../lib/src/sembast_cache_manager_base.dart | 103 + 13 files changed, 2223 insertions(+), 2211 deletions(-) delete mode 100644 packages/isar/lib/data_layer/models/db/db_event.g.dart create mode 100644 packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.dart create mode 100644 packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.g.dart create mode 100644 packages/objectbox/lib/data_layer/db/object_box/schema/db_filter_fetched_range_record.dart diff --git a/packages/isar/lib/data_layer/data_sources/isar_db.dart b/packages/isar/lib/data_layer/data_sources/isar_db.dart index 44ab4527e..658f5ef3c 100644 --- a/packages/isar/lib/data_layer/data_sources/isar_db.dart +++ b/packages/isar/lib/data_layer/data_sources/isar_db.dart @@ -4,6 +4,7 @@ import 'package:isar/isar.dart'; import '../models/db/db_contact_list.dart'; import '../models/db/db_event.dart'; +import '../models/db/db_filter_fetched_range_record.dart'; import '../models/db/db_metadata.dart'; import '../models/db/db_nip05.dart'; import '../models/db/db_relay_set.dart'; @@ -46,7 +47,8 @@ class IsarDbDs { DbRelaySetSchema, DbContactListSchema, DbMetadataSchema, - DbNip05Schema + DbNip05Schema, + DbFilterFetchedRangeRecordSchema, ], ); // isar.write((isar) { diff --git a/packages/isar/lib/data_layer/models/db/db_event.g.dart b/packages/isar/lib/data_layer/models/db/db_event.g.dart deleted file mode 100644 index 99d58637c..000000000 --- a/packages/isar/lib/data_layer/models/db/db_event.g.dart +++ /dev/null @@ -1,2169 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'db_event.dart'; - -// ************************************************************************** -// _IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, invalid_use_of_protected_member, lines_longer_than_80_chars, constant_identifier_names, avoid_js_rounded_ints, no_leading_underscores_for_local_identifiers, require_trailing_commas, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_in_if_null_operators, library_private_types_in_public_api, prefer_const_constructors -// ignore_for_file: type=lint - -extension GetDbEventCollection on Isar { - IsarCollection get dbEvents => this.collection(); -} - -const DbEventSchema = IsarGeneratedSchema( - schema: IsarSchema( - name: 'DbEvent', - idName: 'id', - embedded: false, - properties: [ - IsarPropertySchema( - name: 'id', - type: IsarType.string, - ), - IsarPropertySchema( - name: 'pubKey', - type: IsarType.string, - ), - IsarPropertySchema( - name: 'kind', - type: IsarType.long, - ), - IsarPropertySchema( - name: 'createdAt', - type: IsarType.long, - ), - IsarPropertySchema( - name: 'tags', - type: IsarType.json, - ), - IsarPropertySchema( - name: 'pTags', - type: IsarType.stringList, - ), - IsarPropertySchema( - name: 'content', - type: IsarType.string, - ), - IsarPropertySchema( - name: 'sig', - type: IsarType.string, - ), - IsarPropertySchema( - name: 'validSig', - type: IsarType.bool, - ), - IsarPropertySchema( - name: 'sources', - type: IsarType.stringList, - ), - ], - indexes: [], - ), - converter: IsarObjectConverter( - serialize: serializeDbEvent, - deserialize: deserializeDbEvent, - deserializeProperty: deserializeDbEventProp, - ), - embeddedSchemas: [], -); - -@isarProtected -int serializeDbEvent(IsarWriter writer, DbEvent object) { - IsarCore.writeString(writer, 1, object.id); - IsarCore.writeString(writer, 2, object.pubKey); - IsarCore.writeLong(writer, 3, object.kind); - IsarCore.writeLong(writer, 4, object.createdAt); - IsarCore.writeString(writer, 5, isarJsonEncode(object.tags)); - { - final list = object.pTags; - final listWriter = IsarCore.beginList(writer, 6, list.length); - for (var i = 0; i < list.length; i++) { - IsarCore.writeString(listWriter, i, list[i]); - } - IsarCore.endList(writer, listWriter); - } - IsarCore.writeString(writer, 7, object.content); - IsarCore.writeString(writer, 8, object.sig ?? ''); - { - final value = object.validSig; - if (value == null) { - IsarCore.writeNull(writer, 9); - } else { - IsarCore.writeBool(writer, 9, value); - } - } - { - final list = object.sources; - final listWriter = IsarCore.beginList(writer, 10, list.length); - for (var i = 0; i < list.length; i++) { - IsarCore.writeString(listWriter, i, list[i]); - } - IsarCore.endList(writer, listWriter); - } - return Isar.fastHash(object.id); -} - -@isarProtected -DbEvent deserializeDbEvent(IsarReader reader) { - final String _pubKey; - _pubKey = IsarCore.readString(reader, 2) ?? ''; - final int _kind; - _kind = IsarCore.readLong(reader, 3); - final int _createdAt; - { - final value = IsarCore.readLong(reader, 4); - if (value == -9223372036854775808) { - _createdAt = 0; - } else { - _createdAt = value; - } - } - final List> _tags; - { - final json = isarJsonDecode(IsarCore.readString(reader, 5) ?? 'null'); - if (json is List) { - _tags = castToListOfListOfString(json); - } else { - _tags = const >[]; - } - } - final String _content; - _content = IsarCore.readString(reader, 7) ?? ''; - final String _sig; - _sig = IsarCore.readString(reader, 8) ?? ''; - final bool? _validSig; - { - if (IsarCore.readNull(reader, 9)) { - _validSig = null; - } else { - _validSig = IsarCore.readBool(reader, 9); - } - } - final List _sources; - { - final length = IsarCore.readList(reader, 10, IsarCore.readerPtrPtr); - { - final reader = IsarCore.readerPtr; - if (reader.isNull) { - _sources = const []; - } else { - final list = List.filled(length, '', growable: true); - for (var i = 0; i < length; i++) { - list[i] = IsarCore.readString(reader, i) ?? ''; - } - IsarCore.freeReader(reader); - _sources = list; - } - } - } - final object = DbEvent( - pubKey: _pubKey, - kind: _kind, - createdAt: _createdAt, - tags: _tags, - content: _content, - sig: _sig, - validSig: _validSig, - sources: _sources, - ); - return object; -} - -@isarProtected -dynamic deserializeDbEventProp(IsarReader reader, int property) { - switch (property) { - case 1: - return IsarCore.readString(reader, 1) ?? ''; - case 2: - return IsarCore.readString(reader, 2) ?? ''; - case 3: - return IsarCore.readLong(reader, 3); - case 4: - { - final value = IsarCore.readLong(reader, 4); - if (value == -9223372036854775808) { - return 0; - } else { - return value; - } - } - case 5: - { - final json = isarJsonDecode(IsarCore.readString(reader, 5) ?? 'null'); - if (json is List) { - return json; - } else { - return const []; - } - } - case 6: - { - final length = IsarCore.readList(reader, 6, IsarCore.readerPtrPtr); - { - final reader = IsarCore.readerPtr; - if (reader.isNull) { - return const []; - } else { - final list = List.filled(length, '', growable: true); - for (var i = 0; i < length; i++) { - list[i] = IsarCore.readString(reader, i) ?? ''; - } - IsarCore.freeReader(reader); - return list; - } - } - } - case 7: - return IsarCore.readString(reader, 7) ?? ''; - case 8: - return IsarCore.readString(reader, 8) ?? ''; - case 9: - { - if (IsarCore.readNull(reader, 9)) { - return null; - } else { - return IsarCore.readBool(reader, 9); - } - } - case 10: - { - final length = IsarCore.readList(reader, 10, IsarCore.readerPtrPtr); - { - final reader = IsarCore.readerPtr; - if (reader.isNull) { - return const []; - } else { - final list = List.filled(length, '', growable: true); - for (var i = 0; i < length; i++) { - list[i] = IsarCore.readString(reader, i) ?? ''; - } - IsarCore.freeReader(reader); - return list; - } - } - } - default: - throw ArgumentError('Unknown property: $property'); - } -} - -sealed class _DbEventUpdate { - bool call({ - required String id, - String? pubKey, - int? kind, - int? createdAt, - String? content, - String? sig, - bool? validSig, - }); -} - -class _DbEventUpdateImpl implements _DbEventUpdate { - const _DbEventUpdateImpl(this.collection); - - final IsarCollection collection; - - @override - bool call({ - required String id, - Object? pubKey = ignore, - Object? kind = ignore, - Object? createdAt = ignore, - Object? content = ignore, - Object? sig = ignore, - Object? validSig = ignore, - }) { - return collection.updateProperties([ - id - ], { - if (pubKey != ignore) 2: pubKey as String?, - if (kind != ignore) 3: kind as int?, - if (createdAt != ignore) 4: createdAt as int?, - if (content != ignore) 7: content as String?, - if (sig != ignore) 8: sig as String?, - if (validSig != ignore) 9: validSig as bool?, - }) > - 0; - } -} - -sealed class _DbEventUpdateAll { - int call({ - required List id, - String? pubKey, - int? kind, - int? createdAt, - String? content, - String? sig, - bool? validSig, - }); -} - -class _DbEventUpdateAllImpl implements _DbEventUpdateAll { - const _DbEventUpdateAllImpl(this.collection); - - final IsarCollection collection; - - @override - int call({ - required List id, - Object? pubKey = ignore, - Object? kind = ignore, - Object? createdAt = ignore, - Object? content = ignore, - Object? sig = ignore, - Object? validSig = ignore, - }) { - return collection.updateProperties(id, { - if (pubKey != ignore) 2: pubKey as String?, - if (kind != ignore) 3: kind as int?, - if (createdAt != ignore) 4: createdAt as int?, - if (content != ignore) 7: content as String?, - if (sig != ignore) 8: sig as String?, - if (validSig != ignore) 9: validSig as bool?, - }); - } -} - -extension DbEventUpdate on IsarCollection { - _DbEventUpdate get update => _DbEventUpdateImpl(this); - - _DbEventUpdateAll get updateAll => _DbEventUpdateAllImpl(this); -} - -sealed class _DbEventQueryUpdate { - int call({ - String? pubKey, - int? kind, - int? createdAt, - String? content, - String? sig, - bool? validSig, - }); -} - -class _DbEventQueryUpdateImpl implements _DbEventQueryUpdate { - const _DbEventQueryUpdateImpl(this.query, {this.limit}); - - final IsarQuery query; - final int? limit; - - @override - int call({ - Object? pubKey = ignore, - Object? kind = ignore, - Object? createdAt = ignore, - Object? content = ignore, - Object? sig = ignore, - Object? validSig = ignore, - }) { - return query.updateProperties(limit: limit, { - if (pubKey != ignore) 2: pubKey as String?, - if (kind != ignore) 3: kind as int?, - if (createdAt != ignore) 4: createdAt as int?, - if (content != ignore) 7: content as String?, - if (sig != ignore) 8: sig as String?, - if (validSig != ignore) 9: validSig as bool?, - }); - } -} - -extension DbEventQueryUpdate on IsarQuery { - _DbEventQueryUpdate get updateFirst => - _DbEventQueryUpdateImpl(this, limit: 1); - - _DbEventQueryUpdate get updateAll => _DbEventQueryUpdateImpl(this); -} - -class _DbEventQueryBuilderUpdateImpl implements _DbEventQueryUpdate { - const _DbEventQueryBuilderUpdateImpl(this.query, {this.limit}); - - final QueryBuilder query; - final int? limit; - - @override - int call({ - Object? pubKey = ignore, - Object? kind = ignore, - Object? createdAt = ignore, - Object? content = ignore, - Object? sig = ignore, - Object? validSig = ignore, - }) { - final q = query.build(); - try { - return q.updateProperties(limit: limit, { - if (pubKey != ignore) 2: pubKey as String?, - if (kind != ignore) 3: kind as int?, - if (createdAt != ignore) 4: createdAt as int?, - if (content != ignore) 7: content as String?, - if (sig != ignore) 8: sig as String?, - if (validSig != ignore) 9: validSig as bool?, - }); - } finally { - q.close(); - } - } -} - -extension DbEventQueryBuilderUpdate - on QueryBuilder { - _DbEventQueryUpdate get updateFirst => - _DbEventQueryBuilderUpdateImpl(this, limit: 1); - - _DbEventQueryUpdate get updateAll => _DbEventQueryBuilderUpdateImpl(this); -} - -extension DbEventQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 1, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains(String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 1, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 1, - value: '', - ), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 1, - value: '', - ), - ); - }); - } - - QueryBuilder pubKeyEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyGreaterThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - pubKeyGreaterThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyLessThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyLessThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyBetween( - String lower, - String upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 2, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 2, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pubKeyIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 2, - value: '', - ), - ); - }); - } - - QueryBuilder pubKeyIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 2, - value: '', - ), - ); - }); - } - - QueryBuilder kindEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder kindGreaterThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder - kindGreaterThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder kindLessThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder kindLessThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder kindBetween( - int lower, - int upper, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 3, - lower: lower, - upper: upper, - ), - ); - }); - } - - QueryBuilder createdAtEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 4, - value: value, - ), - ); - }); - } - - QueryBuilder createdAtGreaterThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 4, - value: value, - ), - ); - }); - } - - QueryBuilder - createdAtGreaterThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 4, - value: value, - ), - ); - }); - } - - QueryBuilder createdAtLessThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 4, - value: value, - ), - ); - }); - } - - QueryBuilder - createdAtLessThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 4, - value: value, - ), - ); - }); - } - - QueryBuilder createdAtBetween( - int lower, - int upper, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 4, - lower: lower, - upper: upper, - ), - ); - }); - } - - QueryBuilder pTagsElementEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementGreaterThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - pTagsElementGreaterThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementLessThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - pTagsElementLessThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementBetween( - String lower, - String upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 6, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 6, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 6, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder pTagsElementIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 6, - value: '', - ), - ); - }); - } - - QueryBuilder - pTagsElementIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 6, - value: '', - ), - ); - }); - } - - QueryBuilder pTagsIsEmpty() { - return not().pTagsIsNotEmpty(); - } - - QueryBuilder pTagsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterOrEqualCondition(property: 6, value: null), - ); - }); - } - - QueryBuilder contentEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentGreaterThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - contentGreaterThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentLessThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - contentLessThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentBetween( - String lower, - String upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 7, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 7, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 7, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder contentIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 7, - value: '', - ), - ); - }); - } - - QueryBuilder contentIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 7, - value: '', - ), - ); - }); - } - - QueryBuilder sigEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigGreaterThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigGreaterThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigLessThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigLessThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigBetween( - String lower, - String upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 8, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 8, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 8, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sigIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 8, - value: '', - ), - ); - }); - } - - QueryBuilder sigIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 8, - value: '', - ), - ); - }); - } - - QueryBuilder validSigIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const IsNullCondition(property: 9)); - }); - } - - QueryBuilder validSigIsNotNull() { - return QueryBuilder.apply(not(), (query) { - return query.addFilterCondition(const IsNullCondition(property: 9)); - }); - } - - QueryBuilder validSigEqualTo( - bool? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 9, - value: value, - ), - ); - }); - } - - QueryBuilder sourcesElementEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - sourcesElementGreaterThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - sourcesElementGreaterThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sourcesElementLessThan( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - sourcesElementLessThanOrEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sourcesElementBetween( - String lower, - String upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 10, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - sourcesElementStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sourcesElementEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sourcesElementContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 10, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder sourcesElementMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 10, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - sourcesElementIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 10, - value: '', - ), - ); - }); - } - - QueryBuilder - sourcesElementIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 10, - value: '', - ), - ); - }); - } - - QueryBuilder sourcesIsEmpty() { - return not().sourcesIsNotEmpty(); - } - - QueryBuilder sourcesIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterOrEqualCondition(property: 10, value: null), - ); - }); - } -} - -extension DbEventQueryObject - on QueryBuilder {} - -extension DbEventQuerySortBy on QueryBuilder { - QueryBuilder sortById( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 1, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByIdDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 1, - sort: Sort.desc, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByPubKey( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 2, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByPubKeyDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 2, - sort: Sort.desc, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByKind() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3); - }); - } - - QueryBuilder sortByKindDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3, sort: Sort.desc); - }); - } - - QueryBuilder sortByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(4); - }); - } - - QueryBuilder sortByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(4, sort: Sort.desc); - }); - } - - QueryBuilder sortByTags() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(5); - }); - } - - QueryBuilder sortByTagsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(5, sort: Sort.desc); - }); - } - - QueryBuilder sortByContent( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 7, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByContentDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 7, - sort: Sort.desc, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortBySig( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 8, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortBySigDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 8, - sort: Sort.desc, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByValidSig() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(9); - }); - } - - QueryBuilder sortByValidSigDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(9, sort: Sort.desc); - }); - } -} - -extension DbEventQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByIdDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1, sort: Sort.desc, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByPubKey( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(2, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByPubKeyDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(2, sort: Sort.desc, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByKind() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3); - }); - } - - QueryBuilder thenByKindDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3, sort: Sort.desc); - }); - } - - QueryBuilder thenByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(4); - }); - } - - QueryBuilder thenByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(4, sort: Sort.desc); - }); - } - - QueryBuilder thenByTags() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(5); - }); - } - - QueryBuilder thenByTagsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(5, sort: Sort.desc); - }); - } - - QueryBuilder thenByContent( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(7, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByContentDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(7, sort: Sort.desc, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenBySig( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(8, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenBySigDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(8, sort: Sort.desc, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByValidSig() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(9); - }); - } - - QueryBuilder thenByValidSigDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(9, sort: Sort.desc); - }); - } -} - -extension DbEventQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByPubKey( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(2, caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByKind() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(3); - }); - } - - QueryBuilder distinctByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(4); - }); - } - - QueryBuilder distinctByTags() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(5); - }); - } - - QueryBuilder distinctByPTags() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(6); - }); - } - - QueryBuilder distinctByContent( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(7, caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctBySig( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(8, caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByValidSig() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(9); - }); - } - - QueryBuilder distinctBySources() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(10); - }); - } -} - -extension DbEventQueryProperty1 on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } - - QueryBuilder pubKeyProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(2); - }); - } - - QueryBuilder kindProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(3); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(4); - }); - } - - QueryBuilder, QAfterProperty> tagsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(5); - }); - } - - QueryBuilder, QAfterProperty> pTagsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(6); - }); - } - - QueryBuilder contentProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(7); - }); - } - - QueryBuilder sigProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(8); - }); - } - - QueryBuilder validSigProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(9); - }); - } - - QueryBuilder, QAfterProperty> sourcesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(10); - }); - } -} - -extension DbEventQueryProperty2 on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } - - QueryBuilder pubKeyProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(2); - }); - } - - QueryBuilder kindProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(3); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(4); - }); - } - - QueryBuilder), QAfterProperty> tagsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(5); - }); - } - - QueryBuilder), QAfterProperty> pTagsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(6); - }); - } - - QueryBuilder contentProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(7); - }); - } - - QueryBuilder sigProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(8); - }); - } - - QueryBuilder validSigProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(9); - }); - } - - QueryBuilder), QAfterProperty> sourcesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(10); - }); - } -} - -extension DbEventQueryProperty3 - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } - - QueryBuilder pubKeyProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(2); - }); - } - - QueryBuilder kindProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(3); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(4); - }); - } - - QueryBuilder), QOperations> tagsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(5); - }); - } - - QueryBuilder), QOperations> pTagsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(6); - }); - } - - QueryBuilder contentProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(7); - }); - } - - QueryBuilder sigProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(8); - }); - } - - QueryBuilder validSigProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(9); - }); - } - - QueryBuilder), QOperations> sourcesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(10); - }); - } -} diff --git a/packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.dart b/packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.dart new file mode 100644 index 000000000..26e26d9d0 --- /dev/null +++ b/packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.dart @@ -0,0 +1,46 @@ +import 'package:isar/isar.dart'; +import 'package:ndk/ndk.dart'; + +part 'db_filter_fetched_range_record.g.dart'; + +@Collection() +class DbFilterFetchedRangeRecord { + /// Unique key: filterHash:relayUrl:rangeStart + String get id => '$filterHash:$relayUrl:$rangeStart'; + + @Index() + final String filterHash; + + @Index() + final String relayUrl; + + final int rangeStart; + + final int rangeEnd; + + DbFilterFetchedRangeRecord({ + required this.filterHash, + required this.relayUrl, + required this.rangeStart, + required this.rangeEnd, + }); + + static DbFilterFetchedRangeRecord fromFilterFetchedRangeRecord( + FilterFetchedRangeRecord record) { + return DbFilterFetchedRangeRecord( + filterHash: record.filterHash, + relayUrl: record.relayUrl, + rangeStart: record.rangeStart, + rangeEnd: record.rangeEnd, + ); + } + + FilterFetchedRangeRecord toFilterFetchedRangeRecord() { + return FilterFetchedRangeRecord( + filterHash: filterHash, + relayUrl: relayUrl, + rangeStart: rangeStart, + rangeEnd: rangeEnd, + ); + } +} diff --git a/packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.g.dart b/packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.g.dart new file mode 100644 index 000000000..564959b4c --- /dev/null +++ b/packages/isar/lib/data_layer/models/db/db_filter_fetched_range_record.g.dart @@ -0,0 +1,1314 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'db_filter_fetched_range_record.dart'; + +// ************************************************************************** +// _IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, invalid_use_of_protected_member, lines_longer_than_80_chars, constant_identifier_names, avoid_js_rounded_ints, no_leading_underscores_for_local_identifiers, require_trailing_commas, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_in_if_null_operators, library_private_types_in_public_api, prefer_const_constructors +// ignore_for_file: type=lint + +extension GetDbFilterFetchedRangeRecordCollection on Isar { + IsarCollection + get dbFilterFetchedRangeRecords => this.collection(); +} + +const DbFilterFetchedRangeRecordSchema = IsarGeneratedSchema( + schema: IsarSchema( + name: 'DbFilterFetchedRangeRecord', + idName: 'id', + embedded: false, + properties: [ + IsarPropertySchema( + name: 'filterHash', + type: IsarType.string, + ), + IsarPropertySchema( + name: 'relayUrl', + type: IsarType.string, + ), + IsarPropertySchema( + name: 'rangeStart', + type: IsarType.long, + ), + IsarPropertySchema( + name: 'rangeEnd', + type: IsarType.long, + ), + IsarPropertySchema( + name: 'id', + type: IsarType.string, + ), + ], + indexes: [ + IsarIndexSchema( + name: 'filterHash', + properties: [ + "filterHash", + ], + unique: false, + hash: false, + ), + IsarIndexSchema( + name: 'relayUrl', + properties: [ + "relayUrl", + ], + unique: false, + hash: false, + ), + ], + ), + converter: IsarObjectConverter( + serialize: serializeDbFilterFetchedRangeRecord, + deserialize: deserializeDbFilterFetchedRangeRecord, + deserializeProperty: deserializeDbFilterFetchedRangeRecordProp, + ), + embeddedSchemas: [], +); + +@isarProtected +int serializeDbFilterFetchedRangeRecord( + IsarWriter writer, DbFilterFetchedRangeRecord object) { + IsarCore.writeString(writer, 1, object.filterHash); + IsarCore.writeString(writer, 2, object.relayUrl); + IsarCore.writeLong(writer, 3, object.rangeStart); + IsarCore.writeLong(writer, 4, object.rangeEnd); + IsarCore.writeString(writer, 5, object.id); + return Isar.fastHash(object.id); +} + +@isarProtected +DbFilterFetchedRangeRecord deserializeDbFilterFetchedRangeRecord( + IsarReader reader) { + final String _filterHash; + _filterHash = IsarCore.readString(reader, 1) ?? ''; + final String _relayUrl; + _relayUrl = IsarCore.readString(reader, 2) ?? ''; + final int _rangeStart; + _rangeStart = IsarCore.readLong(reader, 3); + final int _rangeEnd; + _rangeEnd = IsarCore.readLong(reader, 4); + final object = DbFilterFetchedRangeRecord( + filterHash: _filterHash, + relayUrl: _relayUrl, + rangeStart: _rangeStart, + rangeEnd: _rangeEnd, + ); + return object; +} + +@isarProtected +dynamic deserializeDbFilterFetchedRangeRecordProp( + IsarReader reader, int property) { + switch (property) { + case 1: + return IsarCore.readString(reader, 1) ?? ''; + case 2: + return IsarCore.readString(reader, 2) ?? ''; + case 3: + return IsarCore.readLong(reader, 3); + case 4: + return IsarCore.readLong(reader, 4); + case 5: + return IsarCore.readString(reader, 5) ?? ''; + default: + throw ArgumentError('Unknown property: $property'); + } +} + +sealed class _DbFilterFetchedRangeRecordUpdate { + bool call({ + required String id, + String? filterHash, + String? relayUrl, + int? rangeStart, + int? rangeEnd, + }); +} + +class _DbFilterFetchedRangeRecordUpdateImpl + implements _DbFilterFetchedRangeRecordUpdate { + const _DbFilterFetchedRangeRecordUpdateImpl(this.collection); + + final IsarCollection collection; + + @override + bool call({ + required String id, + Object? filterHash = ignore, + Object? relayUrl = ignore, + Object? rangeStart = ignore, + Object? rangeEnd = ignore, + }) { + return collection.updateProperties([ + id + ], { + if (filterHash != ignore) 1: filterHash as String?, + if (relayUrl != ignore) 2: relayUrl as String?, + if (rangeStart != ignore) 3: rangeStart as int?, + if (rangeEnd != ignore) 4: rangeEnd as int?, + }) > + 0; + } +} + +sealed class _DbFilterFetchedRangeRecordUpdateAll { + int call({ + required List id, + String? filterHash, + String? relayUrl, + int? rangeStart, + int? rangeEnd, + }); +} + +class _DbFilterFetchedRangeRecordUpdateAllImpl + implements _DbFilterFetchedRangeRecordUpdateAll { + const _DbFilterFetchedRangeRecordUpdateAllImpl(this.collection); + + final IsarCollection collection; + + @override + int call({ + required List id, + Object? filterHash = ignore, + Object? relayUrl = ignore, + Object? rangeStart = ignore, + Object? rangeEnd = ignore, + }) { + return collection.updateProperties(id, { + if (filterHash != ignore) 1: filterHash as String?, + if (relayUrl != ignore) 2: relayUrl as String?, + if (rangeStart != ignore) 3: rangeStart as int?, + if (rangeEnd != ignore) 4: rangeEnd as int?, + }); + } +} + +extension DbFilterFetchedRangeRecordUpdate + on IsarCollection { + _DbFilterFetchedRangeRecordUpdate get update => + _DbFilterFetchedRangeRecordUpdateImpl(this); + + _DbFilterFetchedRangeRecordUpdateAll get updateAll => + _DbFilterFetchedRangeRecordUpdateAllImpl(this); +} + +sealed class _DbFilterFetchedRangeRecordQueryUpdate { + int call({ + String? filterHash, + String? relayUrl, + int? rangeStart, + int? rangeEnd, + }); +} + +class _DbFilterFetchedRangeRecordQueryUpdateImpl + implements _DbFilterFetchedRangeRecordQueryUpdate { + const _DbFilterFetchedRangeRecordQueryUpdateImpl(this.query, {this.limit}); + + final IsarQuery query; + final int? limit; + + @override + int call({ + Object? filterHash = ignore, + Object? relayUrl = ignore, + Object? rangeStart = ignore, + Object? rangeEnd = ignore, + }) { + return query.updateProperties(limit: limit, { + if (filterHash != ignore) 1: filterHash as String?, + if (relayUrl != ignore) 2: relayUrl as String?, + if (rangeStart != ignore) 3: rangeStart as int?, + if (rangeEnd != ignore) 4: rangeEnd as int?, + }); + } +} + +extension DbFilterFetchedRangeRecordQueryUpdate + on IsarQuery { + _DbFilterFetchedRangeRecordQueryUpdate get updateFirst => + _DbFilterFetchedRangeRecordQueryUpdateImpl(this, limit: 1); + + _DbFilterFetchedRangeRecordQueryUpdate get updateAll => + _DbFilterFetchedRangeRecordQueryUpdateImpl(this); +} + +class _DbFilterFetchedRangeRecordQueryBuilderUpdateImpl + implements _DbFilterFetchedRangeRecordQueryUpdate { + const _DbFilterFetchedRangeRecordQueryBuilderUpdateImpl(this.query, + {this.limit}); + + final QueryBuilder query; + final int? limit; + + @override + int call({ + Object? filterHash = ignore, + Object? relayUrl = ignore, + Object? rangeStart = ignore, + Object? rangeEnd = ignore, + }) { + final q = query.build(); + try { + return q.updateProperties(limit: limit, { + if (filterHash != ignore) 1: filterHash as String?, + if (relayUrl != ignore) 2: relayUrl as String?, + if (rangeStart != ignore) 3: rangeStart as int?, + if (rangeEnd != ignore) 4: rangeEnd as int?, + }); + } finally { + q.close(); + } + } +} + +extension DbFilterFetchedRangeRecordQueryBuilderUpdate on QueryBuilder< + DbFilterFetchedRangeRecord, DbFilterFetchedRangeRecord, QOperations> { + _DbFilterFetchedRangeRecordQueryUpdate get updateFirst => + _DbFilterFetchedRangeRecordQueryBuilderUpdateImpl(this, limit: 1); + + _DbFilterFetchedRangeRecordQueryUpdate get updateAll => + _DbFilterFetchedRangeRecordQueryBuilderUpdateImpl(this); +} + +extension DbFilterFetchedRangeRecordQueryFilter on QueryBuilder< + DbFilterFetchedRangeRecord, DbFilterFetchedRangeRecord, QFilterCondition> { + QueryBuilder filterHashEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashGreaterThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashGreaterThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashLessThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashLessThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashBetween( + String lower, + String upper, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 1, + lower: lower, + upper: upper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + StartsWithCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EndsWithCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + filterHashContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + ContainsCondition( + property: 1, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + filterHashMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + MatchesCondition( + property: 1, + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder filterHashIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const EqualCondition( + property: 1, + value: '', + ), + ); + }); + } + + QueryBuilder filterHashIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const GreaterCondition( + property: 1, + value: '', + ), + ); + }); + } + + QueryBuilder relayUrlEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlGreaterThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlGreaterThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlLessThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlLessThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlBetween( + String lower, + String upper, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 2, + lower: lower, + upper: upper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + StartsWithCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EndsWithCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + relayUrlContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + ContainsCondition( + property: 2, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + relayUrlMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + MatchesCondition( + property: 2, + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder relayUrlIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const EqualCondition( + property: 2, + value: '', + ), + ); + }); + } + + QueryBuilder relayUrlIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const GreaterCondition( + property: 2, + value: '', + ), + ); + }); + } + + QueryBuilder rangeStartEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 3, + value: value, + ), + ); + }); + } + + QueryBuilder rangeStartGreaterThan( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 3, + value: value, + ), + ); + }); + } + + QueryBuilder rangeStartGreaterThanOrEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 3, + value: value, + ), + ); + }); + } + + QueryBuilder rangeStartLessThan( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 3, + value: value, + ), + ); + }); + } + + QueryBuilder rangeStartLessThanOrEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 3, + value: value, + ), + ); + }); + } + + QueryBuilder rangeStartBetween( + int lower, + int upper, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 3, + lower: lower, + upper: upper, + ), + ); + }); + } + + QueryBuilder rangeEndEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 4, + value: value, + ), + ); + }); + } + + QueryBuilder rangeEndGreaterThan( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 4, + value: value, + ), + ); + }); + } + + QueryBuilder rangeEndGreaterThanOrEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 4, + value: value, + ), + ); + }); + } + + QueryBuilder rangeEndLessThan( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 4, + value: value, + ), + ); + }); + } + + QueryBuilder rangeEndLessThanOrEqualTo( + int value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 4, + value: value, + ), + ); + }); + } + + QueryBuilder rangeEndBetween( + int lower, + int upper, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 4, + lower: lower, + upper: upper, + ), + ); + }); + } + + QueryBuilder idEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idGreaterThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idGreaterThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idLessThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idLessThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idBetween( + String lower, + String upper, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 5, + lower: lower, + upper: upper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + StartsWithCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EndsWithCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + idContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + ContainsCondition( + property: 5, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + idMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + MatchesCondition( + property: 5, + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder idIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const EqualCondition( + property: 5, + value: '', + ), + ); + }); + } + + QueryBuilder idIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const GreaterCondition( + property: 5, + value: '', + ), + ); + }); + } +} + +extension DbFilterFetchedRangeRecordQueryObject on QueryBuilder< + DbFilterFetchedRangeRecord, DbFilterFetchedRangeRecord, QFilterCondition> {} + +extension DbFilterFetchedRangeRecordQuerySortBy on QueryBuilder< + DbFilterFetchedRangeRecord, DbFilterFetchedRangeRecord, QSortBy> { + QueryBuilder sortByFilterHash({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy( + 1, + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder sortByFilterHashDesc({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy( + 1, + sort: Sort.desc, + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder sortByRelayUrl({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy( + 2, + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder sortByRelayUrlDesc({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy( + 2, + sort: Sort.desc, + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder sortByRangeStart() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(3); + }); + } + + QueryBuilder sortByRangeStartDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(3, sort: Sort.desc); + }); + } + + QueryBuilder sortByRangeEnd() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(4); + }); + } + + QueryBuilder sortByRangeEndDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(4, sort: Sort.desc); + }); + } + + QueryBuilder sortById({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy( + 5, + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder sortByIdDesc({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy( + 5, + sort: Sort.desc, + caseSensitive: caseSensitive, + ); + }); + } +} + +extension DbFilterFetchedRangeRecordQuerySortThenBy on QueryBuilder< + DbFilterFetchedRangeRecord, DbFilterFetchedRangeRecord, QSortThenBy> { + QueryBuilder thenByFilterHash({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(1, caseSensitive: caseSensitive); + }); + } + + QueryBuilder thenByFilterHashDesc({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(1, sort: Sort.desc, caseSensitive: caseSensitive); + }); + } + + QueryBuilder thenByRelayUrl({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(2, caseSensitive: caseSensitive); + }); + } + + QueryBuilder thenByRelayUrlDesc({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(2, sort: Sort.desc, caseSensitive: caseSensitive); + }); + } + + QueryBuilder thenByRangeStart() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(3); + }); + } + + QueryBuilder thenByRangeStartDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(3, sort: Sort.desc); + }); + } + + QueryBuilder thenByRangeEnd() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(4); + }); + } + + QueryBuilder thenByRangeEndDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(4, sort: Sort.desc); + }); + } + + QueryBuilder thenById({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(5, caseSensitive: caseSensitive); + }); + } + + QueryBuilder thenByIdDesc({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(5, sort: Sort.desc, caseSensitive: caseSensitive); + }); + } +} + +extension DbFilterFetchedRangeRecordQueryWhereDistinct on QueryBuilder< + DbFilterFetchedRangeRecord, DbFilterFetchedRangeRecord, QDistinct> { + QueryBuilder distinctByFilterHash({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(1, caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByRelayUrl({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(2, caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByRangeStart() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(3); + }); + } + + QueryBuilder distinctByRangeEnd() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(4); + }); + } +} + +extension DbFilterFetchedRangeRecordQueryProperty1 on QueryBuilder< + DbFilterFetchedRangeRecord, DbFilterFetchedRangeRecord, QProperty> { + QueryBuilder + filterHashProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(1); + }); + } + + QueryBuilder + relayUrlProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(2); + }); + } + + QueryBuilder + rangeStartProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(3); + }); + } + + QueryBuilder + rangeEndProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(4); + }); + } + + QueryBuilder + idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(5); + }); + } +} + +extension DbFilterFetchedRangeRecordQueryProperty2 + on QueryBuilder { + QueryBuilder + filterHashProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(1); + }); + } + + QueryBuilder + relayUrlProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(2); + }); + } + + QueryBuilder + rangeStartProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(3); + }); + } + + QueryBuilder + rangeEndProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(4); + }); + } + + QueryBuilder + idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(5); + }); + } +} + +extension DbFilterFetchedRangeRecordQueryProperty3 + on QueryBuilder { + QueryBuilder + filterHashProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(1); + }); + } + + QueryBuilder + relayUrlProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(2); + }); + } + + QueryBuilder + rangeStartProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(3); + }); + } + + QueryBuilder + rangeEndProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(4); + }); + } + + QueryBuilder + idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(5); + }); + } +} diff --git a/packages/isar/lib/data_layer/models/db/db_metadata.g.dart b/packages/isar/lib/data_layer/models/db/db_metadata.g.dart index 31459f2b2..bb57cd5e0 100644 --- a/packages/isar/lib/data_layer/models/db/db_metadata.g.dart +++ b/packages/isar/lib/data_layer/models/db/db_metadata.g.dart @@ -80,6 +80,10 @@ const DbMetadataSchema = IsarGeneratedSchema( name: 'refreshedTimestamp', type: IsarType.long, ), + IsarPropertySchema( + name: 'sources', + type: IsarType.stringList, + ), IsarPropertySchema( name: 'cleanNip05', type: IsarType.string, @@ -198,12 +202,20 @@ int serializeDbMetadata(IsarWriter writer, DbMetadata object) { IsarCore.writeLong(writer, 14, object.updatedAt ?? -9223372036854775808); IsarCore.writeLong( writer, 15, object.refreshedTimestamp ?? -9223372036854775808); + { + final list = object.sources; + final listWriter = IsarCore.beginList(writer, 16, list.length); + for (var i = 0; i < list.length; i++) { + IsarCore.writeString(listWriter, i, list[i]); + } + IsarCore.endList(writer, listWriter); + } { final value = object.cleanNip05; if (value == null) { - IsarCore.writeNull(writer, 16); + IsarCore.writeNull(writer, 17); } else { - IsarCore.writeString(writer, 16, value); + IsarCore.writeString(writer, 17, value); } } return Isar.fastHash(object.id); @@ -263,6 +275,22 @@ DbMetadata deserializeDbMetadata(IsarReader reader) { updatedAt: _updatedAt, refreshedTimestamp: _refreshedTimestamp, ); + { + final length = IsarCore.readList(reader, 16, IsarCore.readerPtrPtr); + { + final reader = IsarCore.readerPtr; + if (reader.isNull) { + object.sources = const []; + } else { + final list = List.filled(length, '', growable: true); + for (var i = 0; i < length; i++) { + list[i] = IsarCore.readString(reader, i) ?? ''; + } + IsarCore.freeReader(reader); + object.sources = list; + } + } + } return object; } @@ -344,7 +372,24 @@ dynamic deserializeDbMetadataProp(IsarReader reader, int property) { } } case 16: - return IsarCore.readString(reader, 16); + { + final length = IsarCore.readList(reader, 16, IsarCore.readerPtrPtr); + { + final reader = IsarCore.readerPtr; + if (reader.isNull) { + return const []; + } else { + final list = List.filled(length, '', growable: true); + for (var i = 0; i < length; i++) { + list[i] = IsarCore.readString(reader, i) ?? ''; + } + IsarCore.freeReader(reader); + return list; + } + } + } + case 17: + return IsarCore.readString(reader, 17); default: throw ArgumentError('Unknown property: $property'); } @@ -406,7 +451,7 @@ class _DbMetadataUpdateImpl implements _DbMetadataUpdate { if (lud06 != ignore) 13: lud06 as String?, if (updatedAt != ignore) 14: updatedAt as int?, if (refreshedTimestamp != ignore) 15: refreshedTimestamp as int?, - if (cleanNip05 != ignore) 16: cleanNip05 as String?, + if (cleanNip05 != ignore) 17: cleanNip05 as String?, }) > 0; } @@ -466,7 +511,7 @@ class _DbMetadataUpdateAllImpl implements _DbMetadataUpdateAll { if (lud06 != ignore) 13: lud06 as String?, if (updatedAt != ignore) 14: updatedAt as int?, if (refreshedTimestamp != ignore) 15: refreshedTimestamp as int?, - if (cleanNip05 != ignore) 16: cleanNip05 as String?, + if (cleanNip05 != ignore) 17: cleanNip05 as String?, }); } } @@ -530,7 +575,7 @@ class _DbMetadataQueryUpdateImpl implements _DbMetadataQueryUpdate { if (lud06 != ignore) 13: lud06 as String?, if (updatedAt != ignore) 14: updatedAt as int?, if (refreshedTimestamp != ignore) 15: refreshedTimestamp as int?, - if (cleanNip05 != ignore) 16: cleanNip05 as String?, + if (cleanNip05 != ignore) 17: cleanNip05 as String?, }); } } @@ -579,7 +624,7 @@ class _DbMetadataQueryBuilderUpdateImpl implements _DbMetadataQueryUpdate { if (lud06 != ignore) 13: lud06 as String?, if (updatedAt != ignore) 14: updatedAt as int?, if (refreshedTimestamp != ignore) 15: refreshedTimestamp as int?, - if (cleanNip05 != ignore) 16: cleanNip05 as String?, + if (cleanNip05 != ignore) 17: cleanNip05 as String?, }); } finally { q.close(); @@ -3264,17 +3309,210 @@ extension DbMetadataQueryFilter }); } + QueryBuilder + sourcesElementEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementGreaterThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementGreaterThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementLessThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementLessThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementBetween( + String lower, + String upper, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 16, + lower: lower, + upper: upper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + StartsWithCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EndsWithCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + ContainsCondition( + property: 16, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + MatchesCondition( + property: 16, + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + sourcesElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const EqualCondition( + property: 16, + value: '', + ), + ); + }); + } + + QueryBuilder + sourcesElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const GreaterCondition( + property: 16, + value: '', + ), + ); + }); + } + + QueryBuilder sourcesIsEmpty() { + return not().sourcesIsNotEmpty(); + } + + QueryBuilder + sourcesIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const GreaterOrEqualCondition(property: 16, value: null), + ); + }); + } + QueryBuilder cleanNip05IsNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const IsNullCondition(property: 16)); + return query.addFilterCondition(const IsNullCondition(property: 17)); }); } QueryBuilder cleanNip05IsNotNull() { return QueryBuilder.apply(not(), (query) { - return query.addFilterCondition(const IsNullCondition(property: 16)); + return query.addFilterCondition(const IsNullCondition(property: 17)); }); } @@ -3285,7 +3523,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( EqualCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3301,7 +3539,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( GreaterCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3317,7 +3555,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( GreaterOrEqualCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3333,7 +3571,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( LessCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3349,7 +3587,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( LessOrEqualCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3365,7 +3603,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( BetweenCondition( - property: 16, + property: 17, lower: lower, upper: upper, caseSensitive: caseSensitive, @@ -3382,7 +3620,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( StartsWithCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3398,7 +3636,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( EndsWithCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3411,7 +3649,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( ContainsCondition( - property: 16, + property: 17, value: value, caseSensitive: caseSensitive, ), @@ -3425,7 +3663,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( MatchesCondition( - property: 16, + property: 17, wildcard: pattern, caseSensitive: caseSensitive, ), @@ -3438,7 +3676,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( const EqualCondition( - property: 16, + property: 17, value: '', ), ); @@ -3450,7 +3688,7 @@ extension DbMetadataQueryFilter return QueryBuilder.apply(this, (query) { return query.addFilterCondition( const GreaterCondition( - property: 16, + property: 17, value: '', ), ); @@ -3724,7 +3962,7 @@ extension DbMetadataQuerySortBy {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addSortBy( - 16, + 17, caseSensitive: caseSensitive, ); }); @@ -3734,7 +3972,7 @@ extension DbMetadataQuerySortBy {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addSortBy( - 16, + 17, sort: Sort.desc, caseSensitive: caseSensitive, ); @@ -3927,14 +4165,14 @@ extension DbMetadataQuerySortThenBy QueryBuilder thenByCleanNip05( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { - return query.addSortBy(16, caseSensitive: caseSensitive); + return query.addSortBy(17, caseSensitive: caseSensitive); }); } QueryBuilder thenByCleanNip05Desc( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { - return query.addSortBy(16, sort: Sort.desc, caseSensitive: caseSensitive); + return query.addSortBy(17, sort: Sort.desc, caseSensitive: caseSensitive); }); } } @@ -4038,10 +4276,16 @@ extension DbMetadataQueryWhereDistinct }); } + QueryBuilder distinctBySources() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(16); + }); + } + QueryBuilder distinctByCleanNip05( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(16, caseSensitive: caseSensitive); + return query.addDistinctBy(17, caseSensitive: caseSensitive); }); } } @@ -4140,11 +4384,17 @@ extension DbMetadataQueryProperty1 }); } - QueryBuilder cleanNip05Property() { + QueryBuilder, QAfterProperty> sourcesProperty() { return QueryBuilder.apply(this, (query) { return query.addProperty(16); }); } + + QueryBuilder cleanNip05Property() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(17); + }); + } } extension DbMetadataQueryProperty2 @@ -4242,11 +4492,18 @@ extension DbMetadataQueryProperty2 }); } - QueryBuilder cleanNip05Property() { + QueryBuilder), QAfterProperty> + sourcesProperty() { return QueryBuilder.apply(this, (query) { return query.addProperty(16); }); } + + QueryBuilder cleanNip05Property() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(17); + }); + } } extension DbMetadataQueryProperty3 @@ -4345,10 +4602,17 @@ extension DbMetadataQueryProperty3 }); } + QueryBuilder), QOperations> + sourcesProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(16); + }); + } + QueryBuilder cleanNip05Property() { return QueryBuilder.apply(this, (query) { - return query.addProperty(16); + return query.addProperty(17); }); } } diff --git a/packages/isar/lib/data_layer/models/db/db_relay_set.g.dart b/packages/isar/lib/data_layer/models/db/db_relay_set.g.dart index 2afc13a7a..d8e0e1bc5 100644 --- a/packages/isar/lib/data_layer/models/db/db_relay_set.g.dart +++ b/packages/isar/lib/data_layer/models/db/db_relay_set.g.dart @@ -96,7 +96,14 @@ DbRelaySet deserializeDbRelaySet(IsarReader reader) { } } final int _relayMinCountPerPubkey; - _relayMinCountPerPubkey = IsarCore.readLong(reader, 5); + { + final value = IsarCore.readLong(reader, 5); + if (value == -9223372036854775808) { + _relayMinCountPerPubkey = 0; + } else { + _relayMinCountPerPubkey = value; + } + } final List _items; { final length = IsarCore.readList(reader, 6, IsarCore.readerPtrPtr); @@ -161,7 +168,14 @@ dynamic deserializeDbRelaySetProp(IsarReader reader, int property) { } } case 5: - return IsarCore.readLong(reader, 5); + { + final value = IsarCore.readLong(reader, 5); + if (value == -9223372036854775808) { + return 0; + } else { + return value; + } + } case 6: { final length = IsarCore.readList(reader, 6, IsarCore.readerPtrPtr); diff --git a/packages/isar/lib/data_layer/repositories/cache_manager/isar_cache_manager.dart b/packages/isar/lib/data_layer/repositories/cache_manager/isar_cache_manager.dart index 4c2b221d7..1fd3e2a30 100644 --- a/packages/isar/lib/data_layer/repositories/cache_manager/isar_cache_manager.dart +++ b/packages/isar/lib/data_layer/repositories/cache_manager/isar_cache_manager.dart @@ -1,6 +1,7 @@ import 'dart:core'; import 'package:isar/isar.dart'; +import 'package:ndk/domain_layer/entities/filter_fetched_ranges.dart'; import 'package:ndk/domain_layer/repositories/cache_manager.dart'; import 'package:ndk/entities.dart'; import 'package:ndk/shared/logger/logger.dart'; @@ -8,6 +9,7 @@ import 'package:ndk/shared/logger/logger.dart'; import '../../data_sources/isar_db.dart'; import '../../models/db/db_contact_list.dart'; import '../../models/db/db_event.dart'; +import '../../models/db/db_filter_fetched_range_record.dart'; import '../../models/db/db_metadata.dart'; import '../../models/db/db_nip05.dart'; import '../../models/db/db_relay_set.dart'; @@ -430,4 +432,112 @@ class IsarCacheManager extends CacheManager { Future close() async { isar_ds.isar.close(); } + + // ===================== + // Filter Fetched Ranges + // ===================== + + @override + Future saveFilterFetchedRangeRecord( + FilterFetchedRangeRecord record) async { + final startTime = DateTime.now(); + isar_ds.isar.write((isar) { + isar.dbFilterFetchedRangeRecords + .put(DbFilterFetchedRangeRecord.fromFilterFetchedRangeRecord(record)); + }); + final endTime = DateTime.now(); + final duration = endTime.difference(startTime); + Logger.log.t( + "SAVED FilterFetchedRangeRecord took ${duration.inMilliseconds} ms"); + } + + @override + Future saveFilterFetchedRangeRecords( + List records) async { + final startTime = DateTime.now(); + isar_ds.isar.write((isar) { + isar.dbFilterFetchedRangeRecords.putAll(records + .map((r) => DbFilterFetchedRangeRecord.fromFilterFetchedRangeRecord(r)) + .toList()); + }); + final endTime = DateTime.now(); + final duration = endTime.difference(startTime); + Logger.log.t( + "SAVED ${records.length} FilterFetchedRangeRecords took ${duration.inMilliseconds} ms"); + } + + @override + Future> loadFilterFetchedRangeRecords( + String filterHash) async { + return isar_ds.isar.dbFilterFetchedRangeRecords + .where() + .filterHashEqualTo(filterHash) + .findAll() + .map((r) => r.toFilterFetchedRangeRecord()) + .toList(); + } + + @override + Future> loadFilterFetchedRangeRecordsByRelay( + String filterHash, String relayUrl) async { + return isar_ds.isar.dbFilterFetchedRangeRecords + .where() + .filterHashEqualTo(filterHash) + .and() + .relayUrlEqualTo(relayUrl) + .findAll() + .map((r) => r.toFilterFetchedRangeRecord()) + .toList(); + } + + @override + Future> + loadFilterFetchedRangeRecordsByRelayUrl(String relayUrl) async { + return isar_ds.isar.dbFilterFetchedRangeRecords + .where() + .relayUrlEqualTo(relayUrl) + .findAll() + .map((r) => r.toFilterFetchedRangeRecord()) + .toList(); + } + + @override + Future removeFilterFetchedRangeRecords(String filterHash) async { + isar_ds.isar.write((isar) { + isar.dbFilterFetchedRangeRecords + .where() + .filterHashEqualTo(filterHash) + .deleteAll(); + }); + } + + @override + Future removeFilterFetchedRangeRecordsByFilterAndRelay( + String filterHash, String relayUrl) async { + isar_ds.isar.write((isar) { + isar.dbFilterFetchedRangeRecords + .where() + .filterHashEqualTo(filterHash) + .and() + .relayUrlEqualTo(relayUrl) + .deleteAll(); + }); + } + + @override + Future removeFilterFetchedRangeRecordsByRelay(String relayUrl) async { + isar_ds.isar.write((isar) { + isar.dbFilterFetchedRangeRecords + .where() + .relayUrlEqualTo(relayUrl) + .deleteAll(); + }); + } + + @override + Future removeAllFilterFetchedRangeRecords() async { + isar_ds.isar.write((isar) { + isar.dbFilterFetchedRangeRecords.clear(); + }); + } } diff --git a/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart index 165208616..33247db21 100644 --- a/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart +++ b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart @@ -16,8 +16,8 @@ void main() async { tags: [], createdAt: timestamp, ); - event.sign(key.privateKey!); - return event; + return Nip01Utils.signWithPrivateKey( + event: event, privateKey: key.privateKey!); } group('FetchedRanges integration', () { @@ -55,7 +55,7 @@ void main() async { ); // Make query - final response = ndk.requests.query(filters: [filter]); + final response = ndk.requests.query(filter: filter); // Wait for response to complete await response.future; @@ -121,7 +121,7 @@ void main() async { until: 1000, ); - final response = ndk.requests.query(filters: [filter]); + final response = ndk.requests.query(filter: filter); await response.future; await Future.delayed(const Duration(milliseconds: 100)); @@ -175,7 +175,7 @@ void main() async { until: 200, ); - final response = ndk.requests.query(filters: [filter]); + final response = ndk.requests.query(filter: filter); await response.future; await Future.delayed(const Duration(milliseconds: 100)); @@ -223,7 +223,7 @@ void main() async { authors: [key1.publicKey], ); - final response = ndk.requests.query(filters: [filter]); + final response = ndk.requests.query(filter: filter); await response.future; await Future.delayed(const Duration(milliseconds: 100)); diff --git a/packages/objectbox/lib/data_layer/db/object_box/db_object_box.dart b/packages/objectbox/lib/data_layer/db/object_box/db_object_box.dart index 6c3487049..c922149ef 100644 --- a/packages/objectbox/lib/data_layer/db/object_box/db_object_box.dart +++ b/packages/objectbox/lib/data_layer/db/object_box/db_object_box.dart @@ -6,6 +6,7 @@ import 'package:ndk/ndk.dart'; import '../../../objectbox.g.dart'; import 'db_init_object_box.dart'; import 'schema/db_contact_list.dart'; +import 'schema/db_filter_fetched_range_record.dart'; import 'schema/db_metadata.dart'; import 'schema/db_nip_01_event.dart'; import 'schema/db_nip_05.dart'; @@ -591,4 +592,111 @@ class DbObjectBox implements CacheManager { limit: limit, ); } + + // ===================== + // Filter Fetched Ranges + // ===================== + + @override + Future saveFilterFetchedRangeRecord( + FilterFetchedRangeRecord record) async { + await dbRdy; + final box = _objectBox.store.box(); + box.put(DbFilterFetchedRangeRecord.fromNdk(record)); + } + + @override + Future saveFilterFetchedRangeRecords( + List records) async { + await dbRdy; + final box = _objectBox.store.box(); + box.putMany(records.map((r) => DbFilterFetchedRangeRecord.fromNdk(r)).toList()); + } + + @override + Future> loadFilterFetchedRangeRecords( + String filterHash) async { + await dbRdy; + final box = _objectBox.store.box(); + final results = box + .query(DbFilterFetchedRangeRecord_.filterHash.equals(filterHash)) + .build() + .find(); + return results.map((r) => r.toNdk()).toList(); + } + + @override + Future> loadFilterFetchedRangeRecordsByRelay( + String filterHash, String relayUrl) async { + await dbRdy; + final box = _objectBox.store.box(); + final results = box + .query(DbFilterFetchedRangeRecord_.filterHash + .equals(filterHash) + .and(DbFilterFetchedRangeRecord_.relayUrl.equals(relayUrl))) + .build() + .find(); + return results.map((r) => r.toNdk()).toList(); + } + + @override + Future> + loadFilterFetchedRangeRecordsByRelayUrl(String relayUrl) async { + await dbRdy; + final box = _objectBox.store.box(); + final results = box + .query(DbFilterFetchedRangeRecord_.relayUrl.equals(relayUrl)) + .build() + .find(); + return results.map((r) => r.toNdk()).toList(); + } + + @override + Future removeFilterFetchedRangeRecords(String filterHash) async { + await dbRdy; + final box = _objectBox.store.box(); + final existing = box + .query(DbFilterFetchedRangeRecord_.filterHash.equals(filterHash)) + .build() + .find(); + if (existing.isNotEmpty) { + box.removeMany(existing.map((e) => e.dbId).toList()); + } + } + + @override + Future removeFilterFetchedRangeRecordsByFilterAndRelay( + String filterHash, String relayUrl) async { + await dbRdy; + final box = _objectBox.store.box(); + final existing = box + .query(DbFilterFetchedRangeRecord_.filterHash + .equals(filterHash) + .and(DbFilterFetchedRangeRecord_.relayUrl.equals(relayUrl))) + .build() + .find(); + if (existing.isNotEmpty) { + box.removeMany(existing.map((e) => e.dbId).toList()); + } + } + + @override + Future removeFilterFetchedRangeRecordsByRelay(String relayUrl) async { + await dbRdy; + final box = _objectBox.store.box(); + final existing = box + .query(DbFilterFetchedRangeRecord_.relayUrl.equals(relayUrl)) + .build() + .find(); + if (existing.isNotEmpty) { + box.removeMany(existing.map((e) => e.dbId).toList()); + } + } + + @override + Future removeAllFilterFetchedRangeRecords() async { + await dbRdy; + final box = _objectBox.store.box(); + box.removeAll(); + } } diff --git a/packages/objectbox/lib/data_layer/db/object_box/schema/db_filter_fetched_range_record.dart b/packages/objectbox/lib/data_layer/db/object_box/schema/db_filter_fetched_range_record.dart new file mode 100644 index 000000000..44b1546bb --- /dev/null +++ b/packages/objectbox/lib/data_layer/db/object_box/schema/db_filter_fetched_range_record.dart @@ -0,0 +1,51 @@ +import 'package:objectbox/objectbox.dart'; +import 'package:ndk/ndk.dart'; + +@Entity() +class DbFilterFetchedRangeRecord { + @Id() + int dbId = 0; + + /// Hash of the filter (without since/until) + @Index() + @Property() + late String filterHash; + + /// The relay URL + @Index() + @Property() + late String relayUrl; + + /// Start of the covered range + @Property() + late int rangeStart; + + /// End of the covered range + @Property() + late int rangeEnd; + + DbFilterFetchedRangeRecord({ + required this.filterHash, + required this.relayUrl, + required this.rangeStart, + required this.rangeEnd, + }); + + FilterFetchedRangeRecord toNdk() { + return FilterFetchedRangeRecord( + filterHash: filterHash, + relayUrl: relayUrl, + rangeStart: rangeStart, + rangeEnd: rangeEnd, + ); + } + + factory DbFilterFetchedRangeRecord.fromNdk(FilterFetchedRangeRecord record) { + return DbFilterFetchedRangeRecord( + filterHash: record.filterHash, + relayUrl: record.relayUrl, + rangeStart: record.rangeStart, + rangeEnd: record.rangeEnd, + ); + } +} diff --git a/packages/objectbox/lib/objectbox-model.json b/packages/objectbox/lib/objectbox-model.json index 32ef83696..e2d5ac69b 100644 --- a/packages/objectbox/lib/objectbox-model.json +++ b/packages/objectbox/lib/objectbox-model.json @@ -367,10 +367,48 @@ } ], "relations": [] + }, + { + "id": "8:2356961977798202534", + "lastPropertyId": "5:849430668794703479", + "name": "DbFilterFetchedRangeRecord", + "properties": [ + { + "id": "1:3939782810311199282", + "name": "dbId", + "type": 6, + "flags": 1 + }, + { + "id": "2:3545576993042908087", + "name": "filterHash", + "indexId": "2:758461294333956924", + "type": 9, + "flags": 2048 + }, + { + "id": "3:1898730736240140766", + "name": "relayUrl", + "indexId": "3:735436971837042211", + "type": 9, + "flags": 2048 + }, + { + "id": "4:6718626507552401445", + "name": "rangeStart", + "type": 6 + }, + { + "id": "5:849430668794703479", + "name": "rangeEnd", + "type": 6 + } + ], + "relations": [] } ], - "lastEntityId": "7:4509209106406578683", - "lastIndexId": "1:3132180412806400476", + "lastEntityId": "8:2356961977798202534", + "lastIndexId": "3:735436971837042211", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, diff --git a/packages/objectbox/lib/objectbox.g.dart b/packages/objectbox/lib/objectbox.g.dart index 287506ee9..54fbd71c6 100644 --- a/packages/objectbox/lib/objectbox.g.dart +++ b/packages/objectbox/lib/objectbox.g.dart @@ -15,6 +15,7 @@ import 'package:objectbox/objectbox.dart' as obx; import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart'; import 'data_layer/db/object_box/schema/db_contact_list.dart'; +import 'data_layer/db/object_box/schema/db_filter_fetched_range_record.dart'; import 'data_layer/db/object_box/schema/db_metadata.dart'; import 'data_layer/db/object_box/schema/db_nip_01_event.dart'; import 'data_layer/db/object_box/schema/db_nip_05.dart'; @@ -455,6 +456,48 @@ final _entities = [ relations: [], backlinks: [], ), + obx_int.ModelEntity( + id: const obx_int.IdUid(8, 2356961977798202534), + name: 'DbFilterFetchedRangeRecord', + lastPropertyId: const obx_int.IdUid(5, 849430668794703479), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 3939782810311199282), + name: 'dbId', + type: 6, + flags: 1, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 3545576993042908087), + name: 'filterHash', + type: 9, + flags: 2048, + indexId: const obx_int.IdUid(2, 758461294333956924), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 1898730736240140766), + name: 'relayUrl', + type: 9, + flags: 2048, + indexId: const obx_int.IdUid(3, 735436971837042211), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 6718626507552401445), + name: 'rangeStart', + type: 6, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(5, 849430668794703479), + name: 'rangeEnd', + type: 6, + flags: 0, + ), + ], + relations: [], + backlinks: [], + ), ]; /// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter @@ -495,8 +538,8 @@ Future openStore({ obx_int.ModelDefinition getObjectBoxModel() { final model = obx_int.ModelInfo( entities: _entities, - lastEntityId: const obx_int.IdUid(7, 4509209106406578683), - lastIndexId: const obx_int.IdUid(1, 3132180412806400476), + lastEntityId: const obx_int.IdUid(8, 2356961977798202534), + lastIndexId: const obx_int.IdUid(3, 735436971837042211), lastRelationId: const obx_int.IdUid(0, 0), lastSequenceId: const obx_int.IdUid(0, 0), retiredEntityUids: const [], @@ -1074,6 +1117,65 @@ obx_int.ModelDefinition getObjectBoxModel() { return object; }, ), + DbFilterFetchedRangeRecord: + obx_int.EntityDefinition( + model: _entities[7], + toOneRelations: (DbFilterFetchedRangeRecord object) => [], + toManyRelations: (DbFilterFetchedRangeRecord object) => {}, + getId: (DbFilterFetchedRangeRecord object) => object.dbId, + setId: (DbFilterFetchedRangeRecord object, int id) { + object.dbId = id; + }, + objectToFB: (DbFilterFetchedRangeRecord object, fb.Builder fbb) { + final filterHashOffset = fbb.writeString(object.filterHash); + final relayUrlOffset = fbb.writeString(object.relayUrl); + fbb.startTable(6); + fbb.addInt64(0, object.dbId); + fbb.addOffset(1, filterHashOffset); + fbb.addOffset(2, relayUrlOffset); + fbb.addInt64(3, object.rangeStart); + fbb.addInt64(4, object.rangeEnd); + fbb.finish(fbb.endTable()); + return object.dbId; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final filterHashParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 6, ''); + final relayUrlParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 8, ''); + final rangeStartParam = const fb.Int64Reader().vTableGet( + buffer, + rootOffset, + 10, + 0, + ); + final rangeEndParam = const fb.Int64Reader().vTableGet( + buffer, + rootOffset, + 12, + 0, + ); + final object = + DbFilterFetchedRangeRecord( + filterHash: filterHashParam, + relayUrl: relayUrlParam, + rangeStart: rangeStartParam, + rangeEnd: rangeEndParam, + ) + ..dbId = const fb.Int64Reader().vTableGet( + buffer, + rootOffset, + 4, + 0, + ); + + return object; + }, + ), }; return obx_int.ModelDefinition(model, bindings); @@ -1393,3 +1495,32 @@ class DbRelaySet_ { _entities[6].properties[7], ); } + +/// [DbFilterFetchedRangeRecord] entity fields to define ObjectBox queries. +class DbFilterFetchedRangeRecord_ { + /// See [DbFilterFetchedRangeRecord.dbId]. + static final dbId = obx.QueryIntegerProperty( + _entities[7].properties[0], + ); + + /// See [DbFilterFetchedRangeRecord.filterHash]. + static final filterHash = obx.QueryStringProperty( + _entities[7].properties[1], + ); + + /// See [DbFilterFetchedRangeRecord.relayUrl]. + static final relayUrl = obx.QueryStringProperty( + _entities[7].properties[2], + ); + + /// See [DbFilterFetchedRangeRecord.rangeStart]. + static final rangeStart = + obx.QueryIntegerProperty( + _entities[7].properties[3], + ); + + /// See [DbFilterFetchedRangeRecord.rangeEnd]. + static final rangeEnd = obx.QueryIntegerProperty( + _entities[7].properties[4], + ); +} diff --git a/packages/sembast_cache_manager/lib/src/sembast_cache_manager_base.dart b/packages/sembast_cache_manager/lib/src/sembast_cache_manager_base.dart index c7779b238..b8f268953 100644 --- a/packages/sembast_cache_manager/lib/src/sembast_cache_manager_base.dart +++ b/packages/sembast_cache_manager/lib/src/sembast_cache_manager_base.dart @@ -30,6 +30,8 @@ class SembastCacheManager extends CacheManager { late final sembast.StoreRef> _relayListStore; late final sembast.StoreRef> _nip05Store; late final sembast.StoreRef> _relaySetStore; + late final sembast.StoreRef> + _filterFetchedRangeStore; SembastCacheManager(this._database) { _eventsStore = sembast.stringMapStoreFactory.store('events'); @@ -38,6 +40,8 @@ class SembastCacheManager extends CacheManager { _relayListStore = sembast.stringMapStoreFactory.store('relay_lists'); _nip05Store = sembast.stringMapStoreFactory.store('nip05'); _relaySetStore = sembast.stringMapStoreFactory.store('relay_sets'); + _filterFetchedRangeStore = + sembast.stringMapStoreFactory.store('filter_fetched_ranges'); } @override @@ -402,4 +406,103 @@ class SembastCacheManager extends CacheManager { return metadatas; } + + // ===================== + // Filter Fetched Ranges + // ===================== + + @override + Future saveFilterFetchedRangeRecord( + FilterFetchedRangeRecord record) async { + await _filterFetchedRangeStore + .record(record.key) + .put(_database, record.toJson()); + } + + @override + Future saveFilterFetchedRangeRecords( + List records) async { + await _database.transaction((txn) async { + for (final record in records) { + await _filterFetchedRangeStore + .record(record.key) + .put(txn, record.toJson()); + } + }); + } + + @override + Future> loadFilterFetchedRangeRecords( + String filterHash) async { + final finder = sembast.Finder( + filter: sembast.Filter.equals('filterHash', filterHash), + ); + final records = + await _filterFetchedRangeStore.find(_database, finder: finder); + return records + .map((r) => FilterFetchedRangeRecord.fromJson(r.value)) + .toList(); + } + + @override + Future> loadFilterFetchedRangeRecordsByRelay( + String filterHash, String relayUrl) async { + final finder = sembast.Finder( + filter: sembast.Filter.and([ + sembast.Filter.equals('filterHash', filterHash), + sembast.Filter.equals('relayUrl', relayUrl), + ]), + ); + final records = + await _filterFetchedRangeStore.find(_database, finder: finder); + return records + .map((r) => FilterFetchedRangeRecord.fromJson(r.value)) + .toList(); + } + + @override + Future> + loadFilterFetchedRangeRecordsByRelayUrl(String relayUrl) async { + final finder = sembast.Finder( + filter: sembast.Filter.equals('relayUrl', relayUrl), + ); + final records = + await _filterFetchedRangeStore.find(_database, finder: finder); + return records + .map((r) => FilterFetchedRangeRecord.fromJson(r.value)) + .toList(); + } + + @override + Future removeFilterFetchedRangeRecords(String filterHash) async { + final finder = sembast.Finder( + filter: sembast.Filter.equals('filterHash', filterHash), + ); + await _filterFetchedRangeStore.delete(_database, finder: finder); + } + + @override + Future removeFilterFetchedRangeRecordsByFilterAndRelay( + String filterHash, String relayUrl) async { + final finder = sembast.Finder( + filter: sembast.Filter.and([ + sembast.Filter.equals('filterHash', filterHash), + sembast.Filter.equals('relayUrl', relayUrl), + ]), + ); + await _filterFetchedRangeStore.delete(_database, finder: finder); + } + + @override + Future removeFilterFetchedRangeRecordsByRelay(String relayUrl) async { + final finder = sembast.Finder( + filter: sembast.Filter.equals('relayUrl', relayUrl), + ); + await _filterFetchedRangeStore.delete(_database, finder: finder); + } + + @override + Future removeAllFilterFetchedRangeRecords() async { + await _filterFetchedRangeStore.delete(_database); + } } From 54cf6785c4365e9333eff158121893e4a844e568 Mon Sep 17 00:00:00 2001 From: Gringo Date: Fri, 16 Jan 2026 12:31:05 +0100 Subject: [PATCH 12/13] fix: fetched ranges integration test --- .../fetched_ranges_integration_test.dart | 154 ++++++++++-------- 1 file changed, 89 insertions(+), 65 deletions(-) diff --git a/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart index 33247db21..cec09ff43 100644 --- a/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart +++ b/packages/ndk/test/usecases/fetched_ranges/fetched_ranges_integration_test.dart @@ -8,25 +8,23 @@ import '../../mocks/mock_relay.dart'; void main() async { KeyPair key1 = Bip340.generatePrivateKey(); - Nip01Event textNoteWithTimestamp(KeyPair key, int timestamp) { + Nip01Event textNote(KeyPair key) { Nip01Event event = Nip01Event( kind: Nip01Event.kTextNodeKind, pubKey: key.publicKey, - content: "test note at $timestamp", + content: "test note", tags: [], - createdAt: timestamp, + createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000, ); return Nip01Utils.signWithPrivateKey( event: event, privateKey: key.privateKey!); } + Map textNotes = {key1: textNote(key1)}; + group('FetchedRanges integration', () { - test('query automatically records fetched ranges based on event timestamps', + test('query automatically records fetched ranges after EOSE', timeout: const Timeout(Duration(seconds: 5)), () async { - // Create event with specific timestamp - final event1 = textNoteWithTimestamp(key1, 150); - Map textNotes = {key1: event1}; - // Setup mock relay MockRelay relay1 = MockRelay( name: "relay fetched ranges test", @@ -49,9 +47,18 @@ void main() async { await ndk.relays.seedRelaysConnected; + // Define filter with time bounds + final since = DateTime.now() + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch ~/ + 1000; + final until = DateTime.now().millisecondsSinceEpoch ~/ 1000; + final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], + since: since, + until: until, ); // Make query @@ -67,34 +74,26 @@ void main() async { final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); expect(fetchedRanges.isNotEmpty, isTrue, - reason: 'Fetched ranges should be recorded after query'); + reason: 'FetchedRanges should be recorded after query'); expect(fetchedRanges.containsKey(relay1.url), isTrue, - reason: 'Fetched ranges should contain the relay URL'); + reason: 'FetchedRanges should contain the relay URL'); final relayFetchedRanges = fetchedRanges[relay1.url]!; expect(relayFetchedRanges.ranges.isNotEmpty, isTrue, reason: 'Should have at least one range'); - - // Fetched range since should be based on the event's createdAt (150) - // Fetched range until should extend to now (no filter.until specified) - expect(relayFetchedRanges.ranges.first.since, equals(150), - reason: 'Range since should match oldest event timestamp'); - expect(relayFetchedRanges.ranges.first.until, greaterThan(150), - reason: - 'Range until should extend to now (EOSE means no newer events)'); + expect(relayFetchedRanges.ranges.first.since, equals(since), + reason: 'Range since should match filter since'); + expect(relayFetchedRanges.ranges.first.until, equals(until), + reason: 'Range until should match filter until'); await relay1.stopServer(); await ndk.destroy(); }); - test('fetched ranges reflect actual events received, not filter bounds', + test('multiple queries merge fetched ranges', timeout: const Timeout(Duration(seconds: 5)), () async { - // Create event with timestamp 500 - final event1 = textNoteWithTimestamp(key1, 500); - Map textNotes = {key1: event1}; - MockRelay relay1 = MockRelay( - name: "relay bounds test", + name: "relay merge test", explicitPort: 4201, signEvents: false, ); @@ -113,42 +112,50 @@ void main() async { await ndk.relays.seedRelaysConnected; - // Query with wide bounds (100-1000), but event is at 500 - final filter = Filter( + // First query: 100-200 + final filter1 = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], since: 100, - until: 1000, + until: 200, ); - final response = ndk.requests.query(filter: filter); - await response.future; + final response1 = ndk.requests.query(filter: filter1); + await response1.future; await Future.delayed(const Duration(milliseconds: 100)); - final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); + // Second query: 201-300 (adjacent) + final filter2 = Filter( + kinds: [Nip01Event.kTextNodeKind], + authors: [key1.publicKey], + since: 201, + until: 300, + ); + + final response2 = ndk.requests.query(filter: filter2); + await response2.future; + await Future.delayed(const Duration(milliseconds: 100)); + + // Check fetched ranges were merged + final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter1); expect(fetchedRanges.containsKey(relay1.url), isTrue); final relayFetchedRanges = fetchedRanges[relay1.url]!; - // Fetched range since should be event timestamp (500), not filter.since (100) - // Fetched range until should be filter.until (1000) since EOSE confirms no newer events - expect(relayFetchedRanges.ranges.first.since, equals(500), - reason: 'Fetched range since should reflect oldest event timestamp'); - expect(relayFetchedRanges.ranges.first.until, equals(1000), - reason: - 'Fetched range until should be filter.until (EOSE confirms coverage)'); + // Should have merged into 1 range (100-300) + expect(relayFetchedRanges.ranges.length, equals(1), + reason: 'Adjacent ranges should be merged'); + expect(relayFetchedRanges.ranges.first.since, equals(100)); + expect(relayFetchedRanges.ranges.first.until, equals(300)); await relay1.stopServer(); await ndk.destroy(); }); - test('fetched ranges recorded with filter bounds when no events received', + test('findGaps returns correct gaps after query', timeout: const Timeout(Duration(seconds: 5)), () async { - // Empty - no events - Map textNotes = {}; - MockRelay relay1 = MockRelay( - name: "relay empty test", + name: "relay gaps test", explicitPort: 4202, signEvents: false, ); @@ -167,43 +174,44 @@ void main() async { await ndk.relays.seedRelaysConnected; - // Filter with explicit bounds + // Query for 200-300 final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], - since: 100, - until: 200, + since: 200, + until: 300, ); final response = ndk.requests.query(filter: filter); await response.future; await Future.delayed(const Duration(milliseconds: 100)); - final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); - - // No events but filter has bounds = fetched ranges recorded with filter bounds - expect(fetchedRanges.containsKey(relay1.url), isTrue, - reason: 'Fetched ranges should be recorded using filter bounds'); + // Find gaps for 100-500 + final gaps = await ndk.fetchedRanges.findGaps( + filter: filter, + since: 100, + until: 500, + ); - final relayFetchedRanges = fetchedRanges[relay1.url]!; - expect(relayFetchedRanges.ranges.first.since, equals(100)); - expect(relayFetchedRanges.ranges.first.until, equals(200)); + // Should have 2 gaps: 100-199 and 301-500 + expect(gaps.length, equals(2), reason: 'Should have 2 gaps'); + expect(gaps[0].since, equals(100)); + expect(gaps[0].until, equals(199)); + expect(gaps[1].since, equals(301)); + expect(gaps[1].until, equals(500)); await relay1.stopServer(); await ndk.destroy(); }); - test('fetched ranges use event timestamp', + test('getOptimizedFilters returns filters for gaps only', timeout: const Timeout(Duration(seconds: 5)), () async { - // Create event with specific timestamp - final event1 = textNoteWithTimestamp(key1, 100); - MockRelay relay1 = MockRelay( - name: "relay event timestamp test", + name: "relay optimized test", explicitPort: 4203, signEvents: false, ); - await relay1.startServer(textNotes: {key1: event1}); + await relay1.startServer(textNotes: textNotes); final cache = MemCacheManager(); final ndk = Ndk( @@ -218,23 +226,39 @@ void main() async { await ndk.relays.seedRelaysConnected; + // Query for 200-300 final filter = Filter( kinds: [Nip01Event.kTextNodeKind], authors: [key1.publicKey], + since: 200, + until: 300, ); final response = ndk.requests.query(filter: filter); await response.future; await Future.delayed(const Duration(milliseconds: 100)); - final fetchedRanges = await ndk.fetchedRanges.getForFilter(filter); + // Get optimized filters for 100-500 + final optimized = await ndk.fetchedRanges.getOptimizedFilters( + filter: filter, + since: 100, + until: 500, + ); - expect(fetchedRanges.containsKey(relay1.url), isTrue); + expect(optimized.containsKey(relay1.url), isTrue); - final relayFetchedRanges = fetchedRanges[relay1.url]!; - // since = oldest event timestamp, until = now (no filter.until) - expect(relayFetchedRanges.ranges.first.since, equals(100)); - expect(relayFetchedRanges.ranges.first.until, greaterThan(100)); + final filters = optimized[relay1.url]!; + expect(filters.length, equals(2), reason: 'Should have 2 gap filters'); + + // First gap filter: 100-199 + expect(filters[0].since, equals(100)); + expect(filters[0].until, equals(199)); + expect(filters[0].kinds, equals([Nip01Event.kTextNodeKind])); + expect(filters[0].authors, equals([key1.publicKey])); + + // Second gap filter: 301-500 + expect(filters[1].since, equals(301)); + expect(filters[1].until, equals(500)); await relay1.stopServer(); await ndk.destroy(); From 0ea6538474665f8befa75c858ef6a389b93e15e6 Mon Sep 17 00:00:00 2001 From: Gringo Date: Fri, 16 Jan 2026 12:58:39 +0100 Subject: [PATCH 13/13] fix: sha256 convert in isolate --- .../entities/filter_fetched_ranges.dart | 29 ++++++++++++++++--- .../fetched_ranges/fetched_ranges.dart | 6 ++-- .../entities/filter_fetched_ranges_test.dart | 9 ++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/ndk/lib/domain_layer/entities/filter_fetched_ranges.dart b/packages/ndk/lib/domain_layer/entities/filter_fetched_ranges.dart index f465739ee..4659d8bc7 100644 --- a/packages/ndk/lib/domain_layer/entities/filter_fetched_ranges.dart +++ b/packages/ndk/lib/domain_layer/entities/filter_fetched_ranges.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:crypto/crypto.dart'; import 'filter.dart'; +import '../../shared/isolates/isolate_manager.dart'; /// A time range that has been fetched from a relay class TimeRange { @@ -211,7 +212,22 @@ class FilterFetchedRangeRecord { /// Utility to generate a hash for a filter (excluding temporal fields) class FilterFingerprint { /// Generate a stable hash for a filter, excluding since/until/limit + /// Synchronous version - use [generateAsync] for heavy workloads static String generate(Filter filter) { + return _computeFingerprintFromMap(_filterToMap(filter)); + } + + /// Async version using isolate for expensive SHA256 computation + static Future generateAsync(Filter filter) async { + final map = _filterToMap(filter); + return IsolateManager.instance.runInComputeIsolate( + _computeFingerprintFromMap, + map, + ); + } + + /// Convert filter to a serializable map (for isolate transfer) + static Map _filterToMap(Filter filter) { final map = {}; if (filter.ids != null && filter.ids!.isNotEmpty) { @@ -235,9 +251,14 @@ class FilterFingerprint { map['tags'] = sortedTags; } - final jsonStr = jsonEncode(map); - final bytes = utf8.encode(jsonStr); - final digest = sha256.convert(bytes); - return digest.toString().substring(0, 16); // First 16 chars for brevity + return map; } } + +/// Top-level function for isolate - computes SHA256 hash from map +String _computeFingerprintFromMap(Map map) { + final jsonStr = jsonEncode(map); + final bytes = utf8.encode(jsonStr); + final digest = sha256.convert(bytes); + return digest.toString().substring(0, 16); +} diff --git a/packages/ndk/lib/domain_layer/usecases/fetched_ranges/fetched_ranges.dart b/packages/ndk/lib/domain_layer/usecases/fetched_ranges/fetched_ranges.dart index 22d25c9d1..6e8e4aac4 100644 --- a/packages/ndk/lib/domain_layer/usecases/fetched_ranges/fetched_ranges.dart +++ b/packages/ndk/lib/domain_layer/usecases/fetched_ranges/fetched_ranges.dart @@ -12,7 +12,7 @@ class FetchedRanges { /// Get fetched ranges for a filter across all relays Future> getForFilter(Filter filter) async { - final filterHash = FilterFingerprint.generate(filter); + final filterHash = await FilterFingerprint.generateAsync(filter); final records = await _cacheManager.loadFilterFetchedRangeRecords(filterHash); @@ -101,7 +101,7 @@ class FetchedRanges { required int since, required int until, }) async { - final filterHash = FilterFingerprint.generate(filter); + final filterHash = await FilterFingerprint.generateAsync(filter); // Load existing records for this filter/relay final existingRecords = await _cacheManager @@ -139,7 +139,7 @@ class FetchedRanges { /// Clear fetched ranges for a specific filter Future clearForFilter(Filter filter) async { - final filterHash = FilterFingerprint.generate(filter); + final filterHash = await FilterFingerprint.generateAsync(filter); await _cacheManager.removeFilterFetchedRangeRecords(filterHash); } diff --git a/packages/ndk/test/entities/filter_fetched_ranges_test.dart b/packages/ndk/test/entities/filter_fetched_ranges_test.dart index 0070a5948..5e909a52e 100644 --- a/packages/ndk/test/entities/filter_fetched_ranges_test.dart +++ b/packages/ndk/test/entities/filter_fetched_ranges_test.dart @@ -318,6 +318,15 @@ void main() { expect(hash1, isNot(equals(hash2))); expect(hash1, isNot(equals(hash3))); }); + + test('generateAsync produces same result as generate', () async { + final filter = Filter(kinds: [1, 2], authors: ['pubkey1', 'pubkey2']); + + final syncHash = FilterFingerprint.generate(filter); + final asyncHash = await FilterFingerprint.generateAsync(filter); + + expect(asyncHash, equals(syncHash)); + }); }); group('FetchedRangesGap', () {