feat: add pending_changes table (Step 2 — outbound sync queue)

Protocol-agnostic queue for local mutations (flag, move, delete) that
need to be sent to the server. Enables offline-first behaviour: changes
are written here first and drained by the sync worker. Tracks attempt
count and last error for durable retries.

DB schema bumped to v6.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-19 16:08:17 +02:00
co-authored by Claude Sonnet 4.6
parent 475ba34d28
commit c6fb5154fb
2 changed files with 25 additions and 3 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ sync_state (
)
```
### Step 2 — `pending_changes` table `[ ]`
### Step 2 — `pending_changes` table `[x]`
Protocol-agnostic outbound queue. Any local mutation (flag, move, delete) is written
here first. A sync worker drains the queue and sends to server. Enables offline-first.
+24 -2
View File
@@ -80,6 +80,25 @@ class EmailBodies extends Table {
Set<Column> get primaryKey => {emailId};
}
/// Protocol-agnostic outbound change queue.
/// Local mutations are written here before being sent to the server,
/// enabling offline-first behaviour and durable retries.
@DataClassName('PendingChangeRow')
class PendingChanges extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get accountId =>
text().references(Accounts, #id, onDelete: KeyAction.cascade)();
TextColumn get resourceType => text()();
TextColumn get resourceId => text()();
// "flag_seen" | "flag_flagged" | "move" | "delete"
TextColumn get changeType => text()();
// JSON payload, e.g. {"seen": true} or {"dest": "Archive"}
TextColumn get payload => text()();
DateTimeColumn get createdAt => dateTime()();
IntColumn get attempts => integer().withDefault(const Constant(0))();
TextColumn get lastError => text().nullable()();
}
/// Sync checkpoint per (account, resource type).
/// Stores the server-side state token used for incremental sync.
/// For JMAP: the opaque `state` string from Mailbox/get or Email/get.
@@ -111,12 +130,12 @@ class Drafts extends Table {
// ── Database ──────────────────────────────────────────────────────────────────
@DriftDatabase(tables: [Accounts, Mailboxes, Emails, EmailBodies, Drafts, SyncStates])
@DriftDatabase(tables: [Accounts, Mailboxes, Emails, EmailBodies, Drafts, SyncStates, PendingChanges])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 5;
int get schemaVersion => 6;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -134,6 +153,9 @@ class AppDatabase extends _$AppDatabase {
if (from < 5) {
await m.createTable(syncStates);
}
if (from < 6) {
await m.createTable(pendingChanges);
}
},
);
}