OfflineOutbox class
Append-only event log of pending Supabase writes that the device couldn't (or won't) flush online immediately.
Each enqueued row represents one logical write intent — e.g. "insert this conversation_messages row" or "upsert this user_topic_progress row". The SyncWorker drains the log when connectivity returns, replaying each event against the live Supabase client.
The schema is deliberately generic (a single outbox_events table with
a payload JSON blob) rather than mirroring every Supabase table.
Reasons:
- Phase 3 only needs the write path to survive offline; reads remain online-first (the chat notifier keeps messages in memory during a session). Denormalized per-table mirrors land later if and when read-path offline support is required.
- Adding a new table to the outbox means a new
tableName/payload shape, not a schema migration on the local DB. - The original plan used Drift; the current riverpod_lint pin made
its analyzer-10 requirement incompatible, so this is the raw
sqlite3equivalent. Seepubspec.yamlfor the resolution history.
Web has no native sqlite — local_vector_store_stub.dart set the
precedent; the matching offline_outbox_stub.dart returns no-op
instances and the Riverpod provider yields null under kIsWeb.
Constructors
Properties
Methods
-
delete(
int id) → void -
Permanently remove an event. Used by the "Needs attention" UI when
the user explicitly discards a row, and for periodic cleanup of
syncedrows older than a retention window (not yet implemented). -
dispose(
) → void -
enqueue(
{required String localId, required String tableName, required String operation, required Map< String, dynamic> payload, DateTime? clientUpdatedAt}) → Future<int> -
Append a write intent. Returns the autoincrement row id.
clientUpdatedAtis the wall-clock at the time of the user action, used by SyncWorker for last-write-wins conflict resolution. -
failedEvents(
{int limit = 50}) → List< OutboxEvent> -
Just the
failedrows — convenience for the "Needs attention" list, which doesn't want to show pending/syncing rows because those resolve on their own. -
initialize(
) → Future< void> -
markFailed(
int id, String error) → void -
Mark an event as failed; SyncWorker will retry with backoff next
cycle. After enough attempts the row stays in
failedand is surfaced in the "Needs attention" UI. -
markPending(
int id) → void -
Flip a
failedrow back topendingso the next SyncWorker drain attempts it again. Used by the "Needs attention" UI's retry button. Resetslast_errorso a stale message doesn't keep showing while the row is back in flight. -
markSynced(
int id, {String? serverId}) → void - Mark an event as successfully replayed. Records the server-assigned id (if any) so future re-emits (e.g. an update after an insert) can reference the right row.
-
markSyncing(
int id) → void - Mark an event as in-flight to a Supabase write. Prevents the SyncWorker from picking it up again if it crashes mid-call.
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
pendingAndFailed(
{int limit = 100}) → List< OutboxEvent> -
Oldest-first list of events eligible for a sync attempt now: all
pending/syncingrows, plusfailedrows that are (a) under maxAttempts and (b) past their exponential-backoff window. Rows past the attempt cap stayfailedand are surfaced in the "Needs attention" UI (where the user can markPending to retry) rather than retried automatically. -
pendingCount(
) → int -
Count of unsynced rows. Used by Settings to surface a "N pending"
hint and by
SyncWorkerto short-circuit when empty. -
toString(
) → String -
A string representation of this object.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited
Static Methods
-
failedRowIsDue(
OutboxEvent e, DateTime nowUtc) → bool - Whether a failed row is due for another automatic attempt: not past maxAttempts, and past an exponential-backoff window since its last attempt (30s, 60s, 120s, … capped at 1h).
Constants
- maxAttempts → const int
- Max automatic retry attempts before a failed row is left alone for manual retry (a poison event must not be re-hammered forever).