Skip to content

fix: Prevent corrupting restored order state on relaunch#589

Open
BraCR10 wants to merge 5 commits into
mainfrom
fix/restore-state-lost-after-relaunch
Open

fix: Prevent corrupting restored order state on relaunch#589
BraCR10 wants to merge 5 commits into
mainfrom
fix/restore-state-lost-after-relaunch

Conversation

@BraCR10

@BraCR10 BraCR10 commented May 7, 2026

Copy link
Copy Markdown
Member

Closes #584

Summary

  • After restore, MostroService._onData was saving relay-replayed events to mostroStorage with DateTime.now() timestamps. Those were newer than the synthetic restore messages (which use orderDetail.createdAt). On relaunch, sync() replayed them last, overwriting the correct restored state.
  • Fix: skip addMessage in _onData when isRestoringProvider is true. Event IDs still register in eventStorage for dedup.

How to test

  1. Restore account with active orders/disputes
  2. Confirm orders show correctly after restore
  3. Force-kill → relaunch the app
  4. Orders should show correct state without manual refresh

Summary by CodeRabbit

  • Bug Fixes
    • Buffer incoming messages during restore to prevent replayed direct messages from being written to storage; buffered events are replayed after restore completes to ensure no data loss.
    • Subscriptions now limit fetched orders during restore to avoid processing historical items prematurely.
  • Documentation
    • Updated restore-mode and dispute-restore docs to describe buffering, deduplication, and restored-state handling.

…restore

During account restoration, live direct messages (DMs) were being written to local storage. This could interfere with the restoration process, potentially leading to an inconsistent or corrupted state where restored data is overwritten by new events, causing issues upon app relaunch. This change prevents new DMs from being saved while a restoration is active, ensuring the integrity of the restored account data.
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6076a4eb-aec8-4750-ae13-c141a8bcf1a7

📥 Commits

Reviewing files that changed from the base of the PR and between 96380fb and 65fd8ad.

📒 Files selected for processing (1)
  • docs/architecture/DISPUTE_CHAT_RESTORE.md
✅ Files skipped from review due to trivial changes (1)
  • docs/architecture/DISPUTE_CHAT_RESTORE.md

Walkthrough

During restore, MostroService buffers incoming relay events instead of persisting decrypted DMs, records event IDs for dedupe, and replays buffered events when restore ends. SubscriptionManager sets the orders subscription limit to 0 while restoring. Documentation was updated to describe these behaviors and mark the dispute-restore fix.

Changes

Restore-mode behavior and docs

Layer / File(s) Summary
MostroService buffering & flush
lib/services/mostro_service.dart
Adds _restoreBuffer, imports restore_mode_provider.dart, buffers incoming NostrEvents in _onData when isRestoringProvider is true, listens for restore completion in init(), and implements _flushRestoreBuffer() to reprocess buffered events.
Subscription guard for orders
lib/features/subscriptions/subscription_manager.dart
Imports restore_mode_provider.dart and sets the orders subscription limit to 0 when restoring; otherwise limit is null.
Docs: session recovery & dispute restore
docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md, docs/architecture/DISPUTE_CHAT_RESTORE.md
Update "Restore Mode Protection" and mark Issue 3 fixed: document that isRestoringProvider prevents mostroStorage.addMessage during restore while still deduplicating via eventStorage, and avoids double state application during synthetic writes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • Catrya
  • mostronatorcoder

Poem

🐇 I nibble at bytes while the flag stays high,
I tuck live whispers where they won't pass by.
When the restore sleeps and the banner falls,
I hop them back home through the service halls.
Replays done — disputes rest tall.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly and concisely describes the main fix: preventing restored order state corruption on relaunch, which directly addresses the core issue.
Linked Issues check ✅ Passed All code changes (buffer incoming events, flush on restore completion, apply limit:0 filter) directly address issue #584's requirement that orders retain correct state after relaunch.
Out of Scope Changes check ✅ Passed All changes are scoped to restore-mode handling: MostroService buffering, SubscriptionManager filter adjustment, and documentation updates—no unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/restore-state-lost-after-relaunch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/services/mostro_service.dart (1)

115-118: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Live events arriving during restore are permanently dropped from mostroStorage

The event ID is committed to eventStore unconditionally at lines 115–118, before the restore guard at lines 166–169. When isRestoringProvider is true, the guard returns early without writing to mostroStorage. After restore completes and isRestoringProvider transitions to false, the eventStore retains those cached IDs indefinitely. If the relay replays or resends those events, eventStore.hasItem(event.id!) at line 112 returns true and the handler exits before reaching the guard — the message is never stored, regardless of relay retries.

Any genuinely live event (e.g. a counterparty payment confirmation arriving while restore was running) is silently and permanently excluded from mostroStorage. The restore process does not invalidate eventStorageProvider or trigger a re-subscription with since filters to recover missed events.

A minimal mitigation is to defer the event ID write until after the restore guard, so the event remains eligible for normal processing once isRestoringProvider transitions to false:

🔧 Proposed fix
  Future<void> _onData(NostrEvent event) async {
    final eventStore = ref.read(eventStorageProvider);

    if (await eventStore.hasItem(event.id!)) return;

-   // Reserve event ID immediately to prevent duplicate processing from multiple relays
-   await eventStore.putItem(event.id!, {
-     'id': event.id,
-     'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
-   });

    // ... (decryption, JSON parsing, restore-payload filters) ...

+   final isRestoring = ref.read(isRestoringProvider);
+
+   if (isRestoring) {
+     logger.i('Restore in progress, skipping storage write for ${msg.action}');
+     return;
+   }
+
+   // Reserve event ID to prevent duplicate processing from multiple relays.
+   // Deferred until after restore guard to allow post-restore reprocessing.
+   await eventStore.putItem(event.id!, {
+     'id': event.id,
+     'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
+   });

-   if (ref.read(isRestoringProvider)) {
-     logger.i('Restore in progress, skipping storage write for ${msg.action}');
-     return;
-   }

    await messageStorage.addMessage(messageKey, msg);

This ensures that events received during restore are not marked as processed in eventStore, allowing them to be correctly stored when they arrive again post-restore.

🤖 Prompt for 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.

In `@lib/services/mostro_service.dart` around lines 115 - 118, The event ID is
being committed to eventStore via eventStore.putItem before the
isRestoringProvider guard, causing live events received during restore to be
marked processed and permanently skipped; move the eventStore.putItem call so it
runs only after the isRestoringProvider check and after the event has been
successfully written to mostroStorage (i.e., only write the ID in the branch
where you call mostroStorage write/success), ensuring
eventStore.hasItem(event.id!) remains false during restore and the event can be
processed/replayed later once isRestoringProvider is false.
🧹 Nitpick comments (1)
lib/services/mostro_service.dart (1)

156-169: 💤 Low value

Minor inefficiency: JSON parsing before the restore guard

MostroMessage.fromJson(result[0]) at line 156 is invoked before the isRestoringProvider check at line 166. During high-volume restore replay, every incoming event pays the full deserialization cost even though the result is discarded. Moving the guard to just after the restore-payload filter (line 154) avoids this work.

♻️ Proposed refactor
+      final isRestoring = ref.read(isRestoringProvider);
+      if (isRestoring) {
+        logger.i('Restore in progress, skipping storage write');
+        return;
+      }
+
       final msg = MostroMessage.fromJson(result[0]);
       final messageStorage = ref.read(mostroStorageProvider);
       final messageKey = decryptedEvent.id ?? event.id ?? 'msg_${DateTime.now().millisecondsSinceEpoch}';
-      if (ref.read(isRestoringProvider)) {
-        logger.i('Restore in progress, skipping storage write for ${msg.action}');
-        return;
-      }
🤖 Prompt for 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.

In `@lib/services/mostro_service.dart` around lines 156 - 169, Move the
isRestoringProvider guard so we skip expensive JSON deserialization when
restoring: check ref.read(isRestoringProvider) (and the existing payload filter
at the top of the handler) before calling MostroMessage.fromJson(result[0]). In
practice, relocate the restore-check above the current MostroMessage.fromJson
call and only construct msg and compute messageKey (using
decryptedEvent.id/event) after the guard passes; preserve the existing
logger.i('Restore in progress...') early-return behavior and keep references to
ref.read(mostroStorageProvider), decryptedEvent, event, and messageKey intact.
🤖 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.

Outside diff comments:
In `@lib/services/mostro_service.dart`:
- Around line 115-118: The event ID is being committed to eventStore via
eventStore.putItem before the isRestoringProvider guard, causing live events
received during restore to be marked processed and permanently skipped; move the
eventStore.putItem call so it runs only after the isRestoringProvider check and
after the event has been successfully written to mostroStorage (i.e., only write
the ID in the branch where you call mostroStorage write/success), ensuring
eventStore.hasItem(event.id!) remains false during restore and the event can be
processed/replayed later once isRestoringProvider is false.

---

Nitpick comments:
In `@lib/services/mostro_service.dart`:
- Around line 156-169: Move the isRestoringProvider guard so we skip expensive
JSON deserialization when restoring: check ref.read(isRestoringProvider) (and
the existing payload filter at the top of the handler) before calling
MostroMessage.fromJson(result[0]). In practice, relocate the restore-check above
the current MostroMessage.fromJson call and only construct msg and compute
messageKey (using decryptedEvent.id/event) after the guard passes; preserve the
existing logger.i('Restore in progress...') early-return behavior and keep
references to ref.read(mostroStorageProvider), decryptedEvent, event, and
messageKey intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 363e3505-e6a5-4e82-ad3b-1de7259320e3

📥 Commits

Reviewing files that changed from the base of the PR and between b8bbee9 and 5c0f3f6.

📒 Files selected for processing (1)
  • lib/services/mostro_service.dart

During account restoration, `MostroService` saved historical relay events with current timestamps. This caused them to be replayed after the authoritative synthetic messages (which use original creation timestamps), leading to incorrect order state after an app relaunch. The `isRestoringProvider` flag now prevents `addMessage` during restore, ensuring only the correct restored state persists.
@BraCR10 BraCR10 requested a review from Catrya May 7, 2026 16:03

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the direction of the fix, but there is still a real correctness issue in the current implementation that makes this unsafe to merge as-is.

The problem is the interaction between the new restore guard and event deduplication in MostroService._onData:

  • event IDs are still registered in eventStorage before the restore guard returns
  • when isRestoringProvider is true, the message is not written to mostroStorage
  • after restore finishes, that event is now considered already seen and will not be reprocessed

So any genuinely live DM that arrives during the restore window can be permanently dropped from mostroStorage. This fixes one corruption mode, but introduces another one: missing live events that happened during restore.

That tradeoff is not safe unless the restore flow explicitly guarantees that every event seen during restore is either:

  1. replayed again after restore, or
  2. intentionally buffered and persisted later, or
  3. guaranteed to be fully represented by the synthetic restore state.

From the code in this PR, I do not see that guarantee. Right now the implementation looks like: dedup yes, persist no, replay no. That can lose data.

Please address that gap before merge, either by moving the restore guard earlier so skipped events are not marked as processed, or by buffering/replaying skipped live events once restore completes, or by documenting and enforcing a stronger invariant that makes the drop impossible in practice.

@BraCR10

BraCR10 commented May 14, 2026

Copy link
Copy Markdown
Member Author

I agree with the direction of the fix, but there is still a real correctness issue in the current implementation that makes this unsafe to merge as-is.

The problem is the interaction between the new restore guard and event deduplication in MostroService._onData:

  • event IDs are still registered in eventStorage before the restore guard returns
  • when isRestoringProvider is true, the message is not written to mostroStorage
  • after restore finishes, that event is now considered already seen and will not be reprocessed

So any genuinely live DM that arrives during the restore window can be permanently dropped from mostroStorage. This fixes one corruption mode, but introduces another one: missing live events that happened during restore.

That tradeoff is not safe unless the restore flow explicitly guarantees that every event seen during restore is either:

  1. replayed again after restore, or
  2. intentionally buffered and persisted later, or
  3. guaranteed to be fully represented by the synthetic restore state.

From the code in this PR, I do not see that guarantee. Right now the implementation looks like: dedup yes, persist no, replay no. That can lose data.

Please address that gap before merge, either by moving the restore guard earlier so skipped events are not marked as processed, or by buffering/replaying skipped live events once restore completes, or by documenting and enforcing a stronger invariant that makes the drop impossible in practice.

I understand the concern. The filter exists because Nostr relays sometimes replay old events with newer timestamps, which caused sync() to end up on a stale state in earlier versions. The risk of losing a genuine live event in the 10 second window is low, but acknowledged. The user can always refresh to recover. I consider it worth a low-priority follow-up issue
but the current implementation solves the issue of wrong state after relaunch the app.

@mostronatorcoder

Copy link
Copy Markdown
Contributor

Thanks for the clarification. I agree that this patch fixes the specific relaunch corruption path you were targeting, and I also agree the probability of a genuinely live DM landing inside that restore window is likely low in practice.

That said, I do not think the reply fully resolves the correctness concern I raised. The current behavior is still: an event can be marked as seen in eventStorage, skipped from mostroStorage, and then never replayed. Manual refresh is a recovery path for the user, but it is not the same as proving the implementation is loss-free during restore.

So my view is:

  • the PR definitely fixes one real bug
  • the introduced tradeoff may be acceptable as an explicit product/engineering decision
  • but it is still a tradeoff, not a full technical resolution of the data-loss concern

Because of that, I am not going to claim my original objection was wrong. If the team decides the low-probability drop during the restore window is acceptable compared with the relaunch corruption it prevents, that is a project call. But I think it should be acknowledged as such, ideally with a follow-up issue tracking either buffering, replay, or moving the dedup boundary.

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Changes Requested

🔴 Critical

  • lib/services/mostro_service.dart — the new if (ref.read(isRestoringProvider)) return; guard is too broad. It skips all incoming DMs while restore is active, but eventStorage.putItem(...) already ran, so those events are permanently marked as seen and will not be replayed later. If a real live message arrives during the restore window, it is silently lost.

⚠️ Warning

  • The fix still relies on a 10-second restore window. Even if the intention is only to block historical relay replay, the current implementation cannot distinguish replayed messages from legitimate live messages. This can reintroduce the same class of corruption or cause message loss.

What to change

  • Scope the protection to restore-generated/historical payloads only, or buffer incoming messages during restore and process them after the restore completes. Do not drop them after reserving the event ID.

✅ Good

  • The root-cause docs were updated consistently.
  • Keeping event IDs in eventStorage for deduplication is correct in principle; the issue is the timing of the storage reservation relative to the restore gate.

@BraCR10

BraCR10 commented May 23, 2026

Copy link
Copy Markdown
Member Author

Hello @Catrya @grunch what do you think? Should this PR include logic to avoid losing events during the 10-second restore window, or is that better handled as a follow-up?

@Catrya

Catrya commented Jun 8, 2026

Copy link
Copy Markdown
Member

Hey @BraCR10, sorry for the delay. I think Mostroneitor has a valid point: a user can restore their account in the middle of an order and, right within those ~10 seconds, receive a status change or another important DM, so I think we should account for that.

The catch is that the event ID gets reserved in eventStorage (the putItem) before the isRestoring check, so the event is marked as processed but never stored → on the next launch hasItem discards it and it's never reprocessed. So it's dedup yes, persist no, replay no = the message is lost.

I'd suggest buffer-and-replay, which seems cleanest:

  • During restore, instead of dropping the live event, push it into a queue (_restoreBuffer).
  • When restore finishes (isRestoring flips to false), go through the buffer and process each event normally.
  • Easiest way is to extract the processing logic from _onData into a _processEvent() method and trigger the flush with a ref.listen(isRestoringProvider) inside MostroService, so it covers both the success and error paths without coupling restore_manager to the buffer.

That way we don't break the dedup contract and no message gets lost. As a bonus, it also fixes another gap: a live event arriving before its session is recreated is currently lost too, and with the buffer it'll match since the session exists by flush time.
Since it touches the same lines as this PR, I'd put it right here instead of as a follow-up. What do you think?

BraCR10 added 2 commits June 10, 2026 15:48
During account restoration, live Nostr events could interfere with the integrity of the restored state, leading to inconsistencies or potential state loss.

This change introduces a mechanism to:
- Buffer all incoming live events via `MostroService` while restoration is active, processing them only after restoration completes.
- Temporarily halt live order subscriptions (`limit: 0`) to prevent new order events from conflicting with historical data.

This ensures a clean restoration process, prevents state corruption, and guarantees that all live events are eventually processed without loss or interference.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/features/subscriptions/subscription_manager.dart (1)

42-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Orders subscription is not refreshed when restore mode flips back to false.

limit is computed from ref.read(isRestoringProvider) at filter-build time (Line 132), but subscriptions are rebuilt from session changes only. If sessions are unchanged on restore completion, the orders subscription keeps restore-time filter settings.

Also applies to: 122-133

🤖 Prompt for 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.

In `@lib/features/subscriptions/subscription_manager.dart` around lines 42 - 47,
The orders subscription keeps restore-time filters because limit is computed
only when filters are built and subscriptions are only rebuilt on session
changes; add a listener for the restore flag so subscriptions are rebuilt when
restore mode flips. In _initSessionListener add a
ref.listen<bool>(isRestoringProvider, ...) that calls
_updateAllSubscriptions(ref.read(sessionNotifierProvider)) (or otherwise
triggers the same rebuild path), and ensure _updateAllSubscriptions (and the
filter-building code that computes limit) reads ref.read/isRestoringProvider at
rebuild time rather than capturing it once earlier.
lib/services/mostro_service.dart (1)

120-127: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Buffered events are effectively dropped due to dedupe short-circuit.

Line 123 reserves the event ID before the restore gate, then _flushRestoreBuffer() replays with _onData (Line 197). On replay, Line 120 returns immediately (hasItem == true), so buffered live events never reach addMessage.

Also applies to: 174-177, 191-198

🤖 Prompt for 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.

In `@lib/services/mostro_service.dart` around lines 120 - 127, The dedupe
short-circuit causes buffered events replayed by _flushRestoreBuffer -> _onData
to be dropped because eventStore.putItem is called before the restore/replay
completes; change the flow so reservation does not block replay: either move the
eventStore.putItem call to after the restore/restore-buffer gate (i.e., only
reserve the ID once _flushRestoreBuffer has replayed buffered events), or add a
replay flag/state (e.g., _isRestoring or pass isReplay into _onData) and modify
the early-return that uses eventStore.hasItem to ignore dedupe when replaying;
update references to eventStore.hasItem, eventStore.putItem,
_flushRestoreBuffer, _onData, and addMessage accordingly so buffered events
reach addMessage during replay.
🤖 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 `@lib/services/mostro_service.dart`:
- Around line 50-55: init() is attaching a new ref.listen to isRestoringProvider
every time, causing duplicate listeners because dispose() only cancels
_ordersSubscription; fix by storing the listener handle when calling ref.listen
(e.g., assign the returned subscription to a field like _restoreListener or
similar) and ensure you cancel/close it in dispose(), and also cancel any
existing _restoreListener before re-assigning in init() so you never accumulate
multiple ref.listen callbacks that call _flushRestoreBuffer().

---

Outside diff comments:
In `@lib/features/subscriptions/subscription_manager.dart`:
- Around line 42-47: The orders subscription keeps restore-time filters because
limit is computed only when filters are built and subscriptions are only rebuilt
on session changes; add a listener for the restore flag so subscriptions are
rebuilt when restore mode flips. In _initSessionListener add a
ref.listen<bool>(isRestoringProvider, ...) that calls
_updateAllSubscriptions(ref.read(sessionNotifierProvider)) (or otherwise
triggers the same rebuild path), and ensure _updateAllSubscriptions (and the
filter-building code that computes limit) reads ref.read/isRestoringProvider at
rebuild time rather than capturing it once earlier.

In `@lib/services/mostro_service.dart`:
- Around line 120-127: The dedupe short-circuit causes buffered events replayed
by _flushRestoreBuffer -> _onData to be dropped because eventStore.putItem is
called before the restore/replay completes; change the flow so reservation does
not block replay: either move the eventStore.putItem call to after the
restore/restore-buffer gate (i.e., only reserve the ID once _flushRestoreBuffer
has replayed buffered events), or add a replay flag/state (e.g., _isRestoring or
pass isReplay into _onData) and modify the early-return that uses
eventStore.hasItem to ignore dedupe when replaying; update references to
eventStore.hasItem, eventStore.putItem, _flushRestoreBuffer, _onData, and
addMessage accordingly so buffered events reach addMessage during replay.
🪄 Autofix (Beta)

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

Run ID: 622889c4-d343-4b54-80df-6d0a732f9d51

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0f3f6 and 96380fb.

📒 Files selected for processing (4)
  • docs/architecture/DISPUTE_CHAT_RESTORE.md
  • docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md
  • lib/features/subscriptions/subscription_manager.dart
  • lib/services/mostro_service.dart
✅ Files skipped from review due to trivial changes (1)
  • docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md

Comment on lines +50 to +55
// Flush buffered live events when restore completes (success or error path)
ref.listen<bool>(isRestoringProvider, (previous, next) {
if (previous == true && next == false) {
unawaited(_flushRestoreBuffer());
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

init() accumulates restore listeners across re-inits.

A new ref.listen is created each init() call, but dispose() only cancels _ordersSubscription. This can trigger duplicate flush attempts and unnecessary async work over time.

Also applies to: 58-61

🤖 Prompt for 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.

In `@lib/services/mostro_service.dart` around lines 50 - 55, init() is attaching a
new ref.listen to isRestoringProvider every time, causing duplicate listeners
because dispose() only cancels _ordersSubscription; fix by storing the listener
handle when calling ref.listen (e.g., assign the returned subscription to a
field like _restoreListener or similar) and ensure you cancel/close it in
dispose(), and also cancel any existing _restoreListener before re-assigning in
init() so you never accumulate multiple ref.listen callbacks that call
_flushRestoreBuffer().

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to prevent restored order/dispute state from being overwritten on app relaunch by avoiding persistence of relay-replayed events during the restore window, so that sync() replays only the authoritative synthetic restore messages.

Changes:

  • Add restore-mode gating in MostroService._onData to avoid writing messages to mostroStorage during restore (and introduce buffering/flush logic).
  • Adjust orders subscription filter during restore (SubscriptionManager) to reduce/avoid historical replay.
  • Update architecture documentation to describe restore-mode protections and mark the dispute restore/relaunch issue as fixed.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
lib/services/mostro_service.dart Adds restore-mode checks around DM processing/storage and introduces a restore buffer + flush mechanism.
lib/features/subscriptions/subscription_manager.dart Modifies orders subscription filter to apply a restore-dependent limit.
docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md Documents restore-mode protections and intended effects on message persistence/state reconstruction.
docs/architecture/DISPUTE_CHAT_RESTORE.md Updates root-cause + fix narrative and marks the relaunch-after-restore issue as fixed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/services/mostro_service.dart
Comment thread lib/services/mostro_service.dart
Comment on lines 129 to 133
return NostrFilter(
kinds: [1059],
p: sessions.map((s) => s.tradeKey.public).toList(),
limit: ref.read(isRestoringProvider) ? 0 : null,
);
Update the documentation for "Issue 3 — Dispute State Not Persisted After Restore + App Kill" in `DISPUTE_CHAT_RESTORE.md`.

This revision clarifies the two-part fix for robust account restoration:
1. `SubscriptionManager` now filters historical order events by passing `limit: 0` to relays during restore.
2. `MostroService._onData` buffers any incoming live events, applying them only after the restoration process has completed.

This ensures a clean and consistent state on app relaunch by preventing interference from live or historical events during the restoration window.

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the current head and I still see one real correctness issue that I would block on.

Blocking issue:

  • SubscriptionManager now computes the orders filter with limit: ref.read(isRestoringProvider) ? 0 : null, but that value is captured when the filter is built.
  • From the changes in this PR, I do not see a corresponding subscription refresh triggered when restore mode flips from true back to false.
  • If the session set is unchanged when restore completes, the existing orders subscription can remain alive with the restore-time limit: 0 filter instead of being rebuilt into the normal runtime filter.

Why this matters:

  • The PR’s safety story depends on restore mode being temporary: block historical replay during restore, then return to the normal live subscription shape afterwards.
  • If the orders subscription is not refreshed on restore exit, the app can stay in the restore-mode subscription shape longer than intended.
  • That makes the fix incomplete because the relay-side protection is no longer scoped cleanly to the restore window.

What I would want:

  • Rebuild or refresh the orders subscription when isRestoringProvider changes, not only when the session set changes.
  • In other words, restore-mode transition itself needs to be a first-class resubscription trigger.

The buffering work in MostroService is the right direction and does address the earlier data-loss issue, but this subscription-lifecycle gap is still a blocker for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Order and dispute state lost after closing the app

3 participants