Skip to content

Deliver Android out-of-app writes to the hub via background token - #238

Merged
MaplumeX merged 1 commit into
mainfrom
feat/mobile-sync-on-main-entry
Oct 17, 2026
Merged

MaplumeX merged 1 commit into
mainfrom
feat/mobile-sync-on-main-entry

Conversation

@MaplumeX

Copy link
Copy Markdown
Owner

Problem

On Android the replica only syncs while the app is in the foreground (ADR-0007). Two kinds of writes happen outside the app and previously sat in a native queue until the main WebView next ran:

  • Notification actions (Complete / Snooze 15m / 60m / Tomorrow) from ReminderActionReceiver and ReminderSnoozeActivity.
  • Status bar quick add from QuickAddActivity.

So a task completed from a notification stayed open on desktop, and a task captured from the status bar never showed up on other devices — the most bug-looking gap in the current behavior.

Solution

While enqueuing, native also enqueues a network-constrained one-shot WorkManager job that posts the write straight to the hub using the device background token. The hub applies the same rules as the client and records a SyncChange, so other devices pick it up through the normal pull.

The native queue stays the single source of pending work: delivery only sets a delivered flag and never removes the entry; JS still drains the queue. Delivered completions and quick adds are no longer handed to JS (the task is already in the hub), while delivered snoozes still are, so the queue key keeps protecting the local temporary alarm until JS writes the snooze into the replica.

Idempotency comes from deterministic writes instead of a dedup table: settled-at is the tap time, repeat instance ids are derived by rule, snooze targets are computed from the tap time, and quick add uses the entry id as the task id with deriveSubtaskId(taskId, index) for subtasks. Replays are discarded by the existing stale/ineligible checks, or recognized by id.

Changes

Shared rules (one copy)

  • resolveReminderAction / snoozeTarget moved from packages/api to @taskora/engine domain (api re-exports; callers unchanged), with tests.
  • New @taskora/shared module quick-add.ts: QuickAddDraft types, toQuickAddDraft, and the pure planQuickAddTask(draft, refs). createFromQuickAddDraft now queries refs and applies the plan; behavior unchanged.

Hub

  • New out-of-app module with POST /reminders/actions and POST /quick-add, both guarded by BackgroundTokenGuard only (session JWTs are rejected, and the background token still cannot reach any other write endpoint).
  • TasksService.complete accepts an optional settledAt; new createFromQuickAdd writes task + reminder + subtasks in one transaction.
  • TasksService.create and the engine backend accept a caller-supplied id: same user + same id is idempotent, another user's id is a 409.
  • Background token is no longer described as read-only — it is a restricted credential (plan read + these two writes). Prisma change is comments only.

Android native

  • ReminderStore.PendingAction gains a persisted delivered flag; ReminderAlarms exposes undeliveredActions / markDelivered and stops handing delivered completions to JS.
  • New ReminderActionDelivery + ReminderActionWorker; ReminderBackground.refreshSoon pulls the plan once after a delivery.
  • Status bar entries get a generated id written into the draft JSON; new QuickAddDelivery + QuickAddDeliveryWorker (reads the reminders plugin's background credential), plus undeliveredQuickAdds / markQuickAddDelivered and drainPendingQuickAdds filtering delivered entries.

JS

  • quickAddEntryId(input) reads the entry id (a submission attribute, not part of the draft); createFromQuickAddDraft(draft, deps, { id }) uses it as the task id and derives subtask ids, and skips writing when the task already exists.
  • Status bar controller passes the id through; mobile wiring updated.

Docs

  • New ADR-0027, amendments to ADR-0014 and local-first-v3 issue 09, reminder-actions spec story 7 revision, and a new CONTEXT.md term Out-of-app Write. Spec and issues under .scratch/android-out-of-app-writes/.

Testing

  • New out-of-app.e2e-spec.ts (13 cases): completion settled-at and change log, repeat instance derivation without double derivation on replay, snooze 15m / tomorrow across a non-server account time zone, stale/missing discard, invalid action 400, quick add with all fields and derived subtask ids, replay no-op, compacted id not rebuilt, trashed project falls back to Inbox, empty title, JWT rejected, background token rejected on POST /tasks.
  • New quick-add-plan.test.ts for the shared plan function; moved reminder-action.test.ts to engine; new tests for entry id parsing, id-based idempotent create in the engine backend and hub REST, and controller id passthrough.
  • Full suites green: api 795, engine 328, mobile 108, desktop 79, ui 1173, frontend 28, backend 440; pnpm -r run typecheck, eslint and tsc --noEmit pass.

Notes / follow-ups

  • Native code was compiled against the Tauri Android sources with cached Kotlin/androidx artifacts (0 errors), but a full Gradle/Android build in CI and the on-device acceptance checklist in issue 04 are still pending.
  • Accepted edge case: a delivered snooze re-applied by JS while offline can overwrite a later edit made elsewhere. Re-applying is still the right trade-off, otherwise the temporary snooze alarm would be dropped.
  • The statusbar plugin reads the reminders plugin's SharedPreferences; storage name and keys are duplicated and must stay in sync.
  • WorkManager may defer work to a maintenance window under Doze, so "delivered within a minute" is typical, not guaranteed.

@MaplumeX
MaplumeX merged commit d757007 into main Oct 17, 2026
4 checks passed
@MaplumeX
MaplumeX deleted the feat/mobile-sync-on-main-entry branch October 17, 2026 13:03
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