Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ uses semantic versioning (`MAJOR.MINOR.PATCH`).

### Fixed

- **Watch: a queued offline switch can no longer be created twice.** The watch
app, its data-layer service and its tile trampoline each drained the offline
switch queue with their own lock, so two of them could submit the same queued
switch at once. They now share one lock across the whole app.

- **Phone: syncing offline actions no longer risks duplicating a front.** A new
sync could previously cancel one already in progress at the moment it was
creating a front, which could create it twice; a running sync is now left to
finish.

- **Signing into another account no longer leaks the previous account's data.**
Signing in or out now clears the local response cache and the offline action
queue, so a new account can't briefly see the prior account's members, fronts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,19 @@ class SyncWorker @AssistedInject constructor(
.setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, java.util.concurrent.TimeUnit.SECONDS)
.build()
// KEEP, not REPLACE: a drain that is already running must not be
// cancelled by a fresh enqueue. createFront is posted before its row
// is deleted (see doWork), so a REPLACE cancellation mid-drain is
// exactly the window that can duplicate a front. With KEEP, an
// enqueue while a run is in flight is dropped rather than cancelling
// it; any rows added after that run snapshotted the queue are picked
// up by the next schedule() (each offline action, plus reconnect,
// calls this). Trading a little latency on the tail for not
// double-creating fronts. True exactly-once still needs the backend
// Idempotency-Key noted in doWork.
WorkManager.getInstance(context).enqueueUniqueWork(
WORK_NAME,
ExistingWorkPolicy.REPLACE,
ExistingWorkPolicy.KEEP,
request,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ class WearStore(
// Serialises the offline-switch queue drain. loadAll()/refreshNow() run from
// many triggers (onResume, nav, manual refresh, post-switch, phone nudge);
// without this, two concurrent drains could both submit the same queued row
// before either removed it, double-creating a front.
private val queueDrainMutex = Mutex()
// before either removed it, double-creating a front. The mutex lives in the
// companion object (see below) so it is shared across every WearStore in the
// process.

val frontingMembers: List<WearMember>
get() {
Expand Down Expand Up @@ -278,6 +279,16 @@ class WearStore(
// or the complications aren't currently in use, no harm done.
systems.lupine.sheaf.wear.complications.requestAllComplicationUpdates(context)
}

companion object {
// Process-wide, not per-instance: the activity, the Data Layer service
// and the quick-switch trampoline each build their own WearStore, but
// they drain one shared SharedPreferences queue. A per-instance mutex
// let two of them snapshot and re-submit the same row, double-creating a
// front. Mirrors WearApiClient.refreshMutex, which was hoisted for the
// same reason.
private val queueDrainMutex = Mutex()
}
}

// ── Shared tile_data writers ────────────────────────────────────────────────
Expand Down
Loading