Adds a document analyzing the order discovery mechanism in Nostrmarket. The document identifies the reasons merchants need to manually refresh to see new orders, instead of receiving them automatically. It analyzes timing window issues, connection stability, subscription state management, and event processing delays. It proposes solutions such as enhanced persistent subscriptions, periodic auto-refresh, WebSocket health monitoring, and event gap detection.
9 KiB
Nostrmarket Order Discovery Analysis
Executive Summary
This document analyzes the order discovery mechanism in the Nostrmarket extension and identifies why merchants must manually refresh to see new orders instead of receiving them automatically through persistent subscriptions.
Current Architecture
Two Subscription Systems
The Nostrmarket extension implements two distinct subscription mechanisms for receiving Nostr events:
1. Persistent Subscriptions (Background Task)
Purpose: Continuous monitoring for new orders, products, and merchant events
Implementation:
- Runs via
wait_for_nostr_events()background task - Initiated on extension startup (15-second delay)
- Creates subscription ID:
nostrmarket-{hash} - Monitors all merchant public keys continuously
Code Location: /nostrmarket/tasks.py:37-49
async def wait_for_nostr_events(nostr_client: NostrClient):
while True:
try:
await subscribe_to_all_merchants()
while True:
message = await nostr_client.get_event()
await process_nostr_message(message)
Subscription Filters:
- Direct messages (kind 4) - for orders
- Stall events (kind 30017)
- Product events (kind 30018)
- Profile updates (kind 0)
2. Temporary Subscriptions (Manual Refresh)
Purpose: Catch up on missed events when merchant clicks "Refresh from Nostr"
Implementation:
- Duration: 10 seconds only
- Triggered by user action
- Creates subscription ID:
merchant-{hash} - Fetches ALL events from time=0
Code Location: /nostrmarket/nostr/nostr_client.py:100-120
async def merchant_temp_subscription(self, pk, duration=10):
dm_filters = self._filters_for_direct_messages([pk], 0)
# ... creates filters with time=0 (all history)
await self.send_req_queue.put(["REQ", subscription_id] + merchant_filters)
asyncio.create_task(unsubscribe_with_delay(subscription_id, duration))
Problem Identification
Why Manual Refresh is Required
Issue 1: Timing Window Problem
The persistent subscription uses timestamps from the last database update:
async def subscribe_to_all_merchants():
last_dm_time = await get_last_direct_messages_created_at()
last_stall_time = await get_last_stall_update_time()
last_prod_time = await get_last_product_update_time()
await nostr_client.subscribe_merchants(
public_keys, last_dm_time, last_stall_time, last_prod_time, 0
)
Problem: Events that occur between:
- The last database update time
- When the subscription becomes active ...are potentially missed
Issue 2: Connection Stability
The WebSocket connection between components may be unstable:
[Nostrmarket] <--WebSocket--> [Nostrclient] <--WebSocket--> [Nostr Relays]
Extension Extension (Global)
Potential failure points:
- Connection drops between nostrmarket → nostrclient
- Connection drops between nostrclient → relays
- Reconnection doesn't re-establish subscriptions
Issue 3: Subscription State Management
Current behavior:
- Single persistent subscription per merchant
- No automatic resubscription on failure
- No heartbeat/keepalive mechanism
- No verification that subscription is active
Issue 4: Event Processing Delays
The startup sequence has intentional delays:
async def _subscribe_to_nostr_client():
await asyncio.sleep(10) # Wait for nostrclient
await nostr_client.run_forever()
async def _wait_for_nostr_events():
await asyncio.sleep(15) # Wait for extension init
await wait_for_nostr_events(nostr_client)
Problem: Orders arriving during initialization are missed
Why Manual Refresh Works
The temporary subscription succeeds because:
- Fetches from time=0: Gets ALL historical events
- Fresh connection: Creates new subscription request
- Immediate processing: No startup delays
- Direct feedback: User sees results immediately
# Temporary subscription uses time=0 (all events)
dm_filters = self._filters_for_direct_messages([pk], 0) # ← 0 means all time
# Persistent subscription uses last update time
dm_filters = self._filters_for_direct_messages(public_keys, dm_time) # ← can miss events
Impact Analysis
User Experience Issues
- Merchants miss orders without manual refresh
- No real-time notifications for new orders
- Uncertainty about order status
- Extra manual steps required
- Delayed order fulfillment
Technical Implications
- Not truly decentralized - requires active monitoring
- Scalability concerns - manual refresh doesn't scale
- Reliability issues - depends on user action
- Performance overhead - fetching all events repeatedly
Recommended Solutions
Solution A: Enhanced Persistent Subscriptions
Implement redundant subscription mechanisms:
class EnhancedSubscriptionManager:
def __init__(self):
self.last_heartbeat = time.time()
self.subscription_active = False
async def maintain_subscription(self):
while True:
if not self.subscription_active or \
time.time() - self.last_heartbeat > 30:
await self.resubscribe_with_overlap()
await asyncio.sleep(10)
async def resubscribe_with_overlap(self):
# Use timestamp with 5-minute overlap
overlap_time = int(time.time()) - 300
await subscribe_to_all_merchants(since=overlap_time)
Solution B: Periodic Auto-Refresh
Add automatic temporary subscriptions:
async def auto_refresh_loop():
while True:
await asyncio.sleep(60) # Every minute
merchants = await get_all_active_merchants()
for merchant in merchants:
await merchant_temp_subscription(merchant.pubkey, duration=5)
Solution C: WebSocket Health Monitoring
Implement connection health checks:
class WebSocketHealthMonitor:
async def check_connection_health(self):
try:
# Send ping to nostrclient
response = await nostr_client.ping(timeout=5)
if not response:
await self.reconnect_and_resubscribe()
except Exception:
await self.reconnect_and_resubscribe()
Solution D: Event Gap Detection
Detect and fill gaps in event sequence:
async def detect_event_gaps():
# Check for gaps in event timestamps
last_known = await get_last_event_time()
current_time = int(time.time())
if current_time - last_known > 60: # 1 minute gap
# Perform temporary subscription to fill gap
await fetch_missing_events(since=last_known)
Implementation Priority
Phase 1: Quick Fixes (1-2 days)
- [DONE] Increase temp subscription duration (10s → 30s)
- [DONE] Add connection health logging
- [DONE] Reduce startup delays
Phase 2: Reliability (3-5 days)
- [TODO] Implement subscription heartbeat
- [TODO] Add automatic resubscription on failure
- [TODO] Create event gap detection
Phase 3: Full Solution (1-2 weeks)
- [TODO] WebSocket connection monitoring
- [TODO] Redundant subscription system
- [TODO] Real-time order notifications
- [TODO] Event deduplication logic
Testing Recommendations
Test Scenarios
- Order during startup: Send order within 15 seconds of server start
- Long-running test: Keep server running for 24 hours, send periodic orders
- Connection interruption: Disconnect nostrclient, send order, reconnect
- High volume: Send 100 orders rapidly
- Network latency: Add artificial delay between components
Monitoring Metrics
- Time between order sent → order discovered
- Percentage of orders requiring manual refresh
- WebSocket connection uptime
- Subscription success rate
- Event processing latency
Conclusion
The current order discovery system relies on manual refresh due to:
- Timing gaps in persistent subscriptions
- Connection stability issues
- Lack of redundancy in subscription management
- No automatic recovery mechanisms
While the temporary subscription (manual refresh) provides a workaround, a proper solution requires implementing connection monitoring, subscription health checks, and automatic gap-filling mechanisms to ensure reliable real-time order discovery.
Appendix: Code References
Key Files
/nostrmarket/tasks.py- Background task management/nostrmarket/nostr/nostr_client.py- Nostr client implementation/nostrmarket/services.py- Order processing logic/nostrmarket/views_api.py- API endpoints for refresh
Relevant Functions
wait_for_nostr_events()- Main event loopsubscribe_to_all_merchants()- Persistent subscriptionmerchant_temp_subscription()- Manual refreshprocess_nostr_message()- Event processing
Document prepared: January 2025
Analysis based on: Nostrmarket v1.0
Status: Active Investigation