Skip to content

feat(llc, persistence): add offline support for reactions - #2847

Open
VelikovPetar wants to merge 10 commits into
masterfrom
feature/FLU-506_add_reactions_offline_support
Open

feat(llc, persistence): add offline support for reactions#2847
VelikovPetar wants to merge 10 commits into
masterfrom
feature/FLU-506_add_reactions_offline_support

Conversation

@VelikovPetar

@VelikovPetar VelikovPetar commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Submit a pull request

Linear: FLU-506

Github Issue: #

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested (add some information if not applicable)

Description of the pull request

Adds offline support for reactions. When offline storage is enabled and a reaction add/remove fails with a transient/offline error, the optimistic change is kept and queued for replay on reconnect instead of being reverted.

Low-level client (stream_chat):

  • New PendingOperation model and ReactionPendingOperation (add/delete) describing a queued reaction op.
  • New PendingOperationReplayer that replays queued operations independently (per-op, single attempt) when connectivity is restored.
  • Channel.sendReaction / Channel.deleteReaction now enqueue a pending operation on a retriable error (when persistence is enabled) rather than rolling back the optimistic state.
  • ChatPersistenceClient interface extended with pending-operation persistence methods.

Persistence (stream_chat_persistence):

  • New PendingOperations Drift table, PendingOperationDao, entity, and mapper.
  • Wired into DriftChatDatabase and StreamChatPersistenceClient.

Tests:

  • Unit tests for the pending-operation model, DAO, and replayer.
  • Updated channel/client tests and mocks.
  • Updated sample_app E2E reactions integration test.

Screenshots / Videos

No UI changes.

Summary by CodeRabbit

  • New Features
    • Reactions can now be sent and deleted while offline.
    • When offline storage is enabled, reaction changes are applied optimistically, queued on temporary connectivity issues, and automatically replayed after reconnection in save order.
  • Bug Fixes
    • Optimistic reaction updates now handle temporary vs terminal failures more accurately (revert on terminal failures; keep/queue on temporary ones).
    • Reconnection replay clears successfully processed items without letting one failure prevent later queued updates.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 635a955c-b87d-425c-922a-1d478af908f5

📥 Commits

Reviewing files that changed from the base of the PR and between 37f972b and 5e99c43.

📒 Files selected for processing (1)
  • packages/stream_chat/test/src/db/chat_persistence_client_test.dart

📝 Walkthrough

Walkthrough

Adds durable pending-operation storage for reaction mutations, queues retriable offline reaction sends/deletes, and replays them in order after websocket recovery. Persistence includes Drift schema, DAO, mappings, client methods, mocks, and tests.

Changes

Offline reaction operations

Layer / File(s) Summary
Reaction queue contract and optimistic handling
packages/stream_chat/lib/src/client/channel.dart, packages/stream_chat/lib/src/client/reaction_pending_operation.dart, packages/stream_chat/lib/src/core/models/pending_operation.dart, packages/stream_chat/lib/src/db/chat_persistence_client.dart, packages/stream_chat/test/src/client/*, packages/stream_chat/test/src/core/models/*, packages/stream_chat/test/src/mocks.dart
Retriable reaction failures are queued when persistence is available, optimistic state is retained, and other failures roll back local state.
Pending-operation database storage
packages/stream_chat_persistence/lib/src/entity/*, packages/stream_chat_persistence/lib/src/mapper/*, packages/stream_chat_persistence/lib/src/dao/*, packages/stream_chat_persistence/lib/src/db/*, packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart, packages/stream_chat_persistence/test/*
Adds the pending-operation Drift table, mappings, DAO, database wiring, schema version, persistence methods, and storage tests.
Reconnect replay orchestration
packages/stream_chat/lib/src/client/client.dart, packages/stream_chat/lib/src/client/pending_operation_replayer.dart, packages/stream_chat/test/src/client/client_test.dart, packages/stream_chat/test/src/client/pending_operation_replayer_test.dart
Replays queued operations after recovery, deletes successful or terminally rejected entries, retains transient failures, and isolates per-operation errors.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StreamChatClient
  participant PendingOperationReplayer
  participant ChatPersistenceClient
  participant StreamChatAPI
  StreamChatClient->>PendingOperationReplayer: replay on connectionRecovered
  PendingOperationReplayer->>ChatPersistenceClient: getPendingOperations
  PendingOperationReplayer->>StreamChatAPI: replay reaction operation
  StreamChatAPI-->>PendingOperationReplayer: success or terminal response
  PendingOperationReplayer->>ChatPersistenceClient: deletePendingOperation
Loading

Possibly related PRs

Suggested reviewers: renefloor, xsahil03x

🚥 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 The title accurately summarizes the main change: offline reaction support backed by persistence.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/FLU-506_add_reactions_offline_support

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.

@github-actions

Copy link
Copy Markdown

⚠️ Database Entity Files Modified

The following database entity files have been modified in this PR:

packages/stream_chat_persistence/lib/src/entity/entity.dart
packages/stream_chat_persistence/lib/src/entity/pending_operations.dart

📝 Remember to:

  1. ✅ Database schema version bumped to 1036.
  2. Update entity schema tests if necessary.

Note: This comment is automatically generated by the CI workflow.

# Conflicts:
#	packages/stream_chat/CHANGELOG.md
// optimistic `ownReactions` for messages that previously had none.
state?.replaceMessage(message);
}
rethrow;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not entirely sure if we should always rethrow here, maybe just in the non-retry-able case?

@VelikovPetar
VelikovPetar marked this pull request as ready for review July 28, 2026 14:05
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.09524% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.87%. Comparing base (688c86d) to head (5e99c43).

Files with missing lines Patch % Lines
...hat/lib/src/client/pending_operation_replayer.dart 94.73% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2847      +/-   ##
==========================================
+ Coverage   72.77%   72.87%   +0.09%     
==========================================
  Files         428      433       +5     
  Lines       27602    27702     +100     
==========================================
+ Hits        20088    20187      +99     
- Misses       7514     7515       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@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: 3

🧹 Nitpick comments (3)
packages/stream_chat_persistence/lib/src/dao/pending_operation_dao.dart (1)

19-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider bounding the queue.

getPendingOperations loads the entire table into memory and the queue has no cap, so a long offline session (or an operation that keeps failing retriably) can grow it without bound. A limit on the read, or a retention cap/TTL on insert, would keep replay predictable.

🤖 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 `@packages/stream_chat_persistence/lib/src/dao/pending_operation_dao.dart`
around lines 19 - 23, Bound the pending-operation queue in getPendingOperations
by applying an explicit maximum read limit while preserving ascending id order,
using the project’s established queue-size configuration or constant if
available. Ensure replay remains predictable without loading the entire
pendingOperations table into memory.
packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart (1)

61-82: 🗄️ Data Integrity & Integration | 🔵 Trivial

Destructive migration will silently drop unreplayed pending operations on future schema bumps.

onUpgrade deletes and recreates every table (including the new pending_operations table) whenever schemaVersion changes. For the other tables this is a harmless cache eviction, but pending_operations now holds durable, not-yet-acknowledged user mutations — a future schema bump before replay would silently drop a user's queued reaction/delete with no error surfaced.

Consider special-casing pending_operations in the migration (e.g. preserve/re-insert its rows across onUpgrade), or at minimum documenting this data-loss risk for maintainers bumping schemaVersion going forward.

🤖 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 `@packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart` around
lines 61 - 82, Update the MigrationStrategy.onUpgrade flow so schema bumps do
not silently delete durable rows from the pending_operations table: preserve and
restore its pending operations while recreating the other tables, or otherwise
explicitly retain that table and migrate it safely. Keep the existing
destructive cache migration for non-durable tables, and ensure queued mutations
remain available for replay after upgrades.
packages/stream_chat/test/src/client/pending_operation_replayer_test.dart (1)

38-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated reaction pending-operation test scaffolding across two files. addOp and stubSendReactionOk are copy-pasted verbatim between the two suites; the shared root cause is a missing common test helper for building/stubbing reaction pending operations.

  • packages/stream_chat/test/src/client/pending_operation_replayer_test.dart#L38-L62: keep as the canonical definition (or move) and have the other site import it.
  • packages/stream_chat/test/src/client/client_test.dart#L5470-L5489: replace this local copy with an import of the shared helper instead of redefining addOp/stubSendReactionOk.
🤖 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 `@packages/stream_chat/test/src/client/pending_operation_replayer_test.dart`
around lines 38 - 62, The reaction pending-operation helpers addOp and
stubSendReactionOk are duplicated across the test suites. Keep or extract the
canonical shared definitions from
packages/stream_chat/test/src/client/pending_operation_replayer_test.dart:38-62,
then update packages/stream_chat/test/src/client/client_test.dart:5470-5489 to
import and reuse them, removing its local copies.
🤖 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 `@packages/stream_chat/lib/src/client/channel.dart`:
- Around line 1685-1695: Update _enqueuePendingOperation to return whether
insertPendingOperation succeeded instead of swallowing the outcome. At both call
sites, including sendReaction, only retain the optimistic state when enqueue
succeeds; when it fails, fall back to state?.replaceMessage(message) so the
local reaction is reconciled.

In `@packages/stream_chat/lib/src/client/pending_operation_replayer.dart`:
- Around line 89-110: Validate operation.targetMessageId before returning either
deferred replay closure in _replayCallFor, and treat a null value as malformed
so the existing caller drops it through its malformed-payload handling. Replace
the deferred non-null assertions used by sendReaction and deleteReaction with
the validated value while preserving the existing replay behavior for valid
operations.

In `@packages/stream_chat/lib/src/db/chat_persistence_client.dart`:
- Around line 359-367: Update the doc comments for insertPendingOperation,
getPendingOperations, and deletePendingOperation to explicitly warn that the
default no-op implementations silently discard pending operations and can
prevent optimistic reaction replay; instruct custom ChatPersistenceClient
implementations to override all three methods together when persistence is
enabled.

---

Nitpick comments:
In `@packages/stream_chat_persistence/lib/src/dao/pending_operation_dao.dart`:
- Around line 19-23: Bound the pending-operation queue in getPendingOperations
by applying an explicit maximum read limit while preserving ascending id order,
using the project’s established queue-size configuration or constant if
available. Ensure replay remains predictable without loading the entire
pendingOperations table into memory.

In `@packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart`:
- Around line 61-82: Update the MigrationStrategy.onUpgrade flow so schema bumps
do not silently delete durable rows from the pending_operations table: preserve
and restore its pending operations while recreating the other tables, or
otherwise explicitly retain that table and migrate it safely. Keep the existing
destructive cache migration for non-durable tables, and ensure queued mutations
remain available for replay after upgrades.

In `@packages/stream_chat/test/src/client/pending_operation_replayer_test.dart`:
- Around line 38-62: The reaction pending-operation helpers addOp and
stubSendReactionOk are duplicated across the test suites. Keep or extract the
canonical shared definitions from
packages/stream_chat/test/src/client/pending_operation_replayer_test.dart:38-62,
then update packages/stream_chat/test/src/client/client_test.dart:5470-5489 to
import and reuse them, removing its local copies.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18d78a56-10bc-41d9-8129-154bb6dd524d

📥 Commits

Reviewing files that changed from the base of the PR and between 44b3bc5 and ff7a4b1.

📒 Files selected for processing (25)
  • packages/stream_chat/CHANGELOG.md
  • packages/stream_chat/lib/src/client/channel.dart
  • packages/stream_chat/lib/src/client/client.dart
  • packages/stream_chat/lib/src/client/pending_operation_replayer.dart
  • packages/stream_chat/lib/src/client/reaction_pending_operation.dart
  • packages/stream_chat/lib/src/core/models/pending_operation.dart
  • packages/stream_chat/lib/src/db/chat_persistence_client.dart
  • packages/stream_chat/lib/stream_chat.dart
  • packages/stream_chat/test/src/client/channel_test.dart
  • packages/stream_chat/test/src/client/client_test.dart
  • packages/stream_chat/test/src/client/pending_operation_replayer_test.dart
  • packages/stream_chat/test/src/core/models/pending_operation_test.dart
  • packages/stream_chat/test/src/mocks.dart
  • packages/stream_chat_persistence/CHANGELOG.md
  • packages/stream_chat_persistence/lib/src/dao/dao.dart
  • packages/stream_chat_persistence/lib/src/dao/pending_operation_dao.dart
  • packages/stream_chat_persistence/lib/src/dao/pending_operation_dao.g.dart
  • packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart
  • packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart
  • packages/stream_chat_persistence/lib/src/entity/entity.dart
  • packages/stream_chat_persistence/lib/src/entity/pending_operations.dart
  • packages/stream_chat_persistence/lib/src/mapper/mapper.dart
  • packages/stream_chat_persistence/lib/src/mapper/pending_operation_mapper.dart
  • packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart
  • packages/stream_chat_persistence/test/src/dao/pending_operation_dao_test.dart

Comment thread packages/stream_chat/lib/src/client/channel.dart
Comment thread packages/stream_chat/lib/src/client/pending_operation_replayer.dart
Comment thread packages/stream_chat/lib/src/db/chat_persistence_client.dart Outdated
- Roll back the optimistic reaction when enqueueing the pending operation
  fails, so the local state can no longer diverge from the server.
- Validate targetMessageId eagerly in the replayer so a malformed operation
  is dropped instead of being retried forever.
- Document the default no-op pending-operation methods on
  ChatPersistenceClient and group them at the bottom of the public surface.
- Add persistence-client delegation tests for the pending-operation methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@VelikovPetar

Copy link
Copy Markdown
Contributor Author

Thanks for the nitpicks — decisions on each:

Bound the queue (pending_operation_dao.dart): leaving unbounded for now. With single-attempt replay, entries are removed on accept or terminal reject, so the queue is bounded by activity within a single offline session; a hard cap raises its own "which end to drop" question.

Destructive migration drops pending ops (drift_chat_database.dart): accepted tradeoff, not changing. The persistence layer is a disposable cache with destructive migration by design; a schemaVersion bump dropping unreplayed operations is understood and acceptable.

Duplicated test helpers: leaving the small duplication. The two suites test different layers (replayer semantics vs. client wiring) and the helpers are tiny.

@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: 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 `@packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart`:
- Around line 4275-4574: The generated Drift artifact was edited directly
instead of being regenerated. Update the source Drift table and database
definitions corresponding to PendingOperations, then run the project’s Drift
code-generation command to recreate drift_chat_database.g.dart and keep the
table, PendingOperationEntity, and PendingOperationsCompanion definitions
synchronized.
- Around line 4315-4358: Regenerate the Drift-generated table code with Drift
2.33.0 so PendingOperationsTable.validateIntegrity includes payload metadata
validation and the required-column missing check for payload. Ensure the
generated handling covers both provided values and inserts that omit the
required converted payload column.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 624dca50-9bf9-44ce-84bb-7c2169f8ad9a

📥 Commits

Reviewing files that changed from the base of the PR and between 2314f94 and 37f972b.

📒 Files selected for processing (2)
  • packages/stream_chat_persistence/lib/src/dao/pending_operation_dao.g.dart
  • packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_chat_persistence/lib/src/dao/pending_operation_dao.g.dart

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.

1 participant