fix: detect waiting-state timeouts and clean up local state - #274
Conversation
- Adds the trait method with SQLite and IndexedDB implementations. - SQLite deletes via the nested JSON order id, mirroring get_trade_by_order_id, because trades.id is a fresh UUID for takers. - IndexedDB is a no-op since trades are not persisted on web yet (#233). - Chat messages are deliberately untouched; groundwork for the timeout cleanup.
…e trade - On Canceled, a trade still in pending/waiting states is deleted together with its in-memory session instead of lingering as a Canceled history row. - Mirrors v1, which deletes pending/waiting sessions on cancel; trades that progressed keep their row and chat as history. - InProgress rows are kept: it only enters via the Kind 38383 sync, where it masks both waiting and active phases, so it is ambiguous.
- mostrod republishes the order as pending BEFORE sending Canceled on a taker-responsible timeout, so the blind remove raced the republish and left the order missing from the book until an app restart. - The book is fed only by Kind 38383 events: a genuine cancel arrives as a status update and the UI already filters non-pending orders.
…stream - New broadcast channel + TradeUpdatesStream mirroring the bond-slashed pattern; the Canceled arm emits for both the wipe and mark-canceled paths. - Needed because polling cannot observe a cancellation anymore: the wiped trade has no DB row left, and a timeout republish reads pending again. - Regenerates frb bindings.
- add-invoice had no status listener at all: after a waiting-state timeout the form stayed up and every submit died with a 10s NoDaemonResponse. - pay-invoice only watched polled terminal statuses, which a wipe or a timeout republish never produces; both screens now also listen to the on_trade_updated push, show a notice, refresh My Trades and leave.
- Runs 60s after subscribing and every 30 min (v1's cadence): waiting trades past their window are checked against the public book — pending republish wipes taker rows and resyncs maker rows, an outright cancel wipes; absence or ambiguous statuses change nothing, the daemon stays the authority. - Also drops keyless in-memory sessions older than 24h (first real caller of cleanup_stale_sessions) and logs sweep counters.
- Documents the on_trade_updated stream, the differentiated Canceled handling (wipe never-active trades vs keep history), the no-book-removal rule and its race rationale, and the stale-state sweep. - Notes timeout_at's real semantics and the trade-row deletion exception in the data model.
…the update - rawTradesProvider now invalidates itself on every on_trade_updated push, so a daemon cancel wipe (or a sweep resync) refreshes the list from any screen instead of only via pull-to-refresh or the invoice screens. - The wipe log moves to the bridge logger so it is visible while testing.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughChangesThe PR adds Rust trade cancellation and stale-state reconciliation, storage cleanup by order ID, a Flutter Rust Bridge update stream, and Flutter listeners that refresh trades and navigate inactive invoice screens home. Trade lifecycle synchronization
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Daemon
participant orders_api
participant TradeUpdatesStream
participant tradeUpdatesProvider
participant InvoiceScreen
Daemon->>orders_api: send cancellation or stale-order status
orders_api->>TradeUpdatesStream: publish TradeUpdate
tradeUpdatesProvider->>TradeUpdatesStream: read next update
TradeUpdatesStream-->>tradeUpdatesProvider: return TradeUpdate
tradeUpdatesProvider-->>InvoiceScreen: deliver matching update
InvoiceScreen->>InvoiceScreen: refresh trades and navigate home
Possibly related issues
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rust/src/api/orders.rs`:
- Around line 3224-3226: Update TradeUpdatesStream::next to handle
RecvError::Lagged(_) as an explicit stable status/code exposed through the Dart
API instead of skipping it and continuing; preserve normal update handling, and
add coverage that sends more than TRADE_UPDATES_CAPACITY updates to a slow
subscriber and verifies the wiped invoice’s Canceled update is surfaced as lag.
In `@specs/004-mostro-p2p-client/data-model.md`:
- Around line 114-117: Revise the trade-row persistence statement in the data
model to cover only daemon cancellation behavior: retain deletion for trades
canceled while pending or waiting and never active, without asserting that
normal updates are limited to status. Ensure the wording remains consistent with
the allowed hold_invoice and amount_sats updates described elsewhere.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 50c81419-9e8f-4898-9caa-98d417b0e8f6
📒 Files selected for processing (13)
lib/features/order/providers/trade_state_provider.dartlib/features/order/screens/add_lightning_invoice_screen.dartlib/features/order/screens/pay_lightning_invoice_screen.dartlib/features/trades/providers/trades_providers.dartrust/src/api/orders.rsrust/src/api/types.rsrust/src/db/indexeddb.rsrust/src/db/mod.rsrust/src/db/sqlite.rsrust/src/frb_generated.rsrust/src/mostro/session.rsspecs/004-mostro-p2p-client/contracts/orders.mdspecs/004-mostro-p2p-client/data-model.md
- Log dropped trade-updates on broadcast lag instead of skipping silently; the Dart-facing lag API was declined as disproportionate (see PR thread). - Fix the data-model persistence wording: trade rows mutate in place (status, hold_invoice, amount_sats), they are not status-only.
There was a problem hiding this comment.
Approved — reviewed at head e1d71fc2.
Verification
- CI green on this head: Rust (build/test/clippy/wasm), Flutter (analyze/test), Web (wasm build).
- Local: full
cargo test --libone1d71fc2— 239 passed, 0 failed, including the four new tests (cancellation_wipes_history_only_for_never_active_trades,sweep_action_requires_a_positive_book_signal,trade_updates_reach_subscribers,delete_trade_by_order_id_removes_only_the_matching_row). git diff --checkclean.
Design review
- Removing the blind
remove_orderfrom theCanceledhandler is correct: the 38383 feed stays the single authority on the book, eliminating the republish race. The two remainingremove_ordercall sites are legitimate (user-initiated optimistic cancel, order-ID reconciliation). cancellation_wipes_historyis appropriately conservative — keepingInProgressrows given mostrod's ambiguousin-progressmasking is the right call, and the fallback on DB lookup error (keep + mark Canceled) fails safe.- The sweep only acts on positive book signals, so a missing/incomplete book fetch can never wipe a live trade; the
timeout_at/started_at + 900age gate matches the daemon's waiting window and the spec. - Wipe +
remove_session+emit_trade_updateare idempotent, so a liveCanceledracing the 30-min sweep is harmless; Dart screens have one-shot_navigatedguards against duplicate emissions. delete_trade_by_order_idcorrectly matchesget_trade_by_order_id's nested-JSON filter (taker rows have a fresh UUID astrades.id), with test coverage proving only the matching row is removed.- Both CodeRabbit threads are addressed on this head (Lagged now logged-and-skipped with a sound degradation argument; data-model wording corrected).
Non-blocking nits
SWEEP_ACTIVEnever resets: if the sweep task ever panics, no sweep runs again until app restart. Fine to leave as-is.- Worth confirming the claim about mostrod's publish order (
pending38383 beforeCanceled) against the current mostrod release — the whole race fix rests on it, and it matches my reading of the daemon's scheduler.
Nice work — the push stream closes a real gap that polling structurally could not observe.
fix #148
When a trade in
waiting-buyer-invoice/waiting-paymenttimed out on thedaemon, the client mishandled it on every front:
the live chip) — no session/state cleanup existed at all.
app restart: mostrod publishes the
pending38383 BEFORE sendingCanceled, and the handler's blindremove_orderraced it.every submit died with a 10s
NoDaemonResponse(mostrod ignores messagesfor canceled orders). Pay-invoice only reacted to polled terminal statuses,
which a timeout republish never produces.
What changed
delete_trade_by_order_id(SQLite + IndexedDB no-op).peer, no chat) is wiped together with its in-memory session, mirroring v1's
session cleanup; trades that progressed keep their row and chat as history.
No more blind book removal — the 38383 feed stays authoritative.
on_trade_updatedpush stream: polling cannot observe a wipe (norow left) or a republish (book reads
pending); the invoice screens andthe My Trades list now react to the push (notice + navigation + refresh).
trades past their window are checked against the public book — pending
republish wipes taker rows / resyncs maker rows to
Pending; outrightcancel wipes; no positive daemon signal → no action. Also drops keyless
in-memory sessions older than 24h. Counters go to the bridge log.
contracts/orders.md+data-model.mdupdated (cancellationsemantics, stream, sweep).
Manual testing
on My Trades. On timeout: log line
Canceled before active — removed trade for order=…, the trade disappears from My Trades, and the order reappearsin the order book immediately (no restart).
pay-invoice) screen → "order no longer active" notice + navigation home.
Pending in book and My Trades (chip AND filter bucket after refresh).
reopen and wait ~60s →
stale sweep: examined=1 wiped=1 …in the log andthe trade is gone.
Out of scope (issues to be opened separately)
Back-navigation showing "Take this order" again; My Trades filter using
stale status; countdown driven by real
expiration_seconds; polling→pushmigration for status chips; configurable history retention.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation