Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions lib/features/subscriptions/subscription_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@ class SubscriptionManager {
.whereType<String>();
await ref.read(chatCursorStoreProvider).warmUp(orderIds);
}
if (type == SubscriptionType.orders) {
// Best-effort: a prefs/storage failure must not prevent the REQ.
try {
await ref
.read(ordersCursorStoreProvider)
.warmUp([ref.read(settingsProvider).mostroPublicKey]);
} catch (e) {
logger.w('Orders cursor warm-up unavailable: $e');
}
}

final filter = _createFilterForType(type, sessions);
if (filter == null) {
Expand Down Expand Up @@ -365,10 +375,23 @@ class SubscriptionManager {
// and re-subscribe when the node info arrives after this subscription.
final transport = _resolveOrdersTransport();
_appliedOrdersTransport = transport;
final mostroPubkey = ref.read(settingsProvider).mostroPublicKey;
// Persisted cursor bounds the replay; fresh installs fall back to the
// default lookback (older history is served by the restore flow).
DateTime? cursorSince;
try {
cursorSince =
ref.read(ordersCursorStoreProvider).cachedSinceFor(mostroPubkey);
} catch (e) {
logger.w('Orders cursor unavailable, using default lookback: $e');
}
final ordersSince = cursorSince ??
DateTime.now().subtract(NostrEventExtensions.chatDefaultLookback);
Comment thread
grunch marked this conversation as resolved.
Outdated
return buildOrdersFilter(
transport,
tradeKeys,
ref.read(settingsProvider).mostroPublicKey,
mostroPubkey,
since: ordersSince,
);
case SubscriptionType.chat:
// Kind 14 chat envelope: filter by the K_sign authors derived from
Expand Down Expand Up @@ -670,19 +693,27 @@ class SubscriptionManager {
NostrFilter buildOrdersFilter(
Transport transport,
List<String> tradeKeys,
String mostroPubkey,
) {
String mostroPubkey, {
DateTime? since,
}) {
switch (transport) {
case Transport.giftWrap:
// Legacy transport: gift wrap timestamps are randomized ±48 h, so a
// cursor since would silently drop messages. Left unbounded until the
// 1059 branch is removed.
return NostrFilter(
kinds: [1059],
p: tradeKeys,
);
case Transport.nip44:
// kind 14 carries real timestamps: bound the replay with the persisted
// cursor (minus its overlap) and a limit, mirroring the chat filters.
return NostrFilter(
kinds: [14],
authors: [mostroPubkey],
p: tradeKeys,
since: since,
limit: NostrEventExtensions.chatDefaultLimit,
);
}
}
12 changes: 12 additions & 0 deletions lib/services/chat_cursor_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class ChatCursorStore {
/// generalization; keeping it preserves cursors stored by older builds.
static const disputeKeyPrefix = 'dispute_chat_since_';

/// Orders (node message) namespace, keyed by the node pubkey: one live
/// orders subscription exists per connected node.
static const ordersKeyPrefix = 'orders_since_';

/// Peer (buyer-seller) chat namespace, keyed by orderId. Shared with the
/// background isolate, which builds its own store without Riverpod.
static const peerKeyPrefix = 'chat_since_';
Expand Down Expand Up @@ -120,3 +124,11 @@ final chatCursorStoreProvider = Provider<ChatCursorStore>(
keyPrefix: ChatCursorStore.peerKeyPrefix,
),
);

/// Orders (node kind-14 message) cursors, keyed by the node pubkey.
final ordersCursorStoreProvider = Provider<ChatCursorStore>(
(ref) => ChatCursorStore(
ref.watch(sharedPreferencesProvider),
keyPrefix: ChatCursorStore.ordersKeyPrefix,
),
);
9 changes: 9 additions & 0 deletions lib/services/mostro_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:dart_nostr/dart_nostr.dart';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mostro_mobile/services/chat_cursor_store.dart';
import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/enums.dart';
import 'package:mostro_mobile/data/models.dart';
Expand Down Expand Up @@ -120,6 +121,14 @@ class MostroService {
/// by every later replay, and the order sat at a stale status across
/// restarts.
Future<void> _markEventProcessed(NostrEvent event) {
// Advance the orders since cursor: a processed event never needs
// replaying. Events left unmarked (e.g. no session yet) stay behind the
// cursor's overlap window so a resubscription can retry them.
unawaited(
ref
.read(ordersCursorStoreProvider)
.advance(_settings.mostroPublicKey, event.createdAt!),
Comment thread
grunch marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
);
return ref.read(eventStorageProvider).putItem(event.id!, {
'id': event.id,
'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
Expand Down
27 changes: 27 additions & 0 deletions test/features/subscriptions/orders_filter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,32 @@ void main() {
expect(filter.authors, [mostroPubkey]);
expect(filter.p, tradeKeys);
});

// Without since/limit every (re)subscription replayed the node's full
// message history from every relay; kind 14 carries real timestamps, so
// a persisted cursor can bound the replay tightly.
test('v2 (nip44) carries the cursor since and a limit', () {
final since = DateTime.fromMillisecondsSinceEpoch(1700000000 * 1000);
final filter = buildOrdersFilter(
Transport.nip44,
tradeKeys,
mostroPubkey,
since: since,
);

expect(filter.since, since);
expect(filter.limit, isNotNull);
});

test('v1 (giftWrap) ignores since: its timestamps are randomized', () {
final filter = buildOrdersFilter(
Transport.giftWrap,
tradeKeys,
mostroPubkey,
since: DateTime.now(),
);

expect(filter.since, isNull);
});
});
}
Loading