Fix button issues, improve layout, and enhance notification handling (#192)

* Fix approval /declien button issue. update action buttons and improve layout

* feat(notifications): enhance notification handling and default templates across components

* fix(sync): improve sync handling to coalesce concurrent requests and prevent lost writes

there is BUG that was very annoying cause by race condition we we update and sync happen as we update and doesn't return the latest added task. this basically fix it
This commit is contained in:
Mohamad Tarbin
2026-08-05 01:14:39 -04:00
committed by GitHub
parent 784ffbcd00
commit 48dbc7a152
2 changed files with 36 additions and 3 deletions

View File

@@ -99,7 +99,8 @@ export const useChores = (includeArchive = false) => {
queryFn: async () => {
if (isOfflineFeatureEnabled()) {
try {
// Sync from server first (no-op if already syncing or offline)
// Sync from server first (coalesced with any run already in flight,
// so a just-created chore can't be missed by a stale cursor)
if (networkManager.isOnline) {
await syncEngine.sync()
}

View File

@@ -28,6 +28,10 @@ class SyncEngine {
constructor() {
this.isSyncing = false
this.listeners = []
// The run currently in flight, and the single follow-up run queued behind
// it. See sync() for why a follow-up is needed rather than just waiting.
this.inFlight = null
this.queued = null
}
// Register listener for sync state changes
@@ -42,10 +46,38 @@ class SyncEngine {
this.listeners.forEach(cb => cb(state))
}
// Main sync entry point — returns true if sync succeeded, false otherwise
// Main sync entry point — returns true if sync succeeded, false otherwise.
//
// Concurrent callers are coalesced rather than dropped. Returning early while
// another run is in flight used to lose writes: that run's /sync/changes
// request may have been issued *before* the caller's change reached the
// server, so its cursor skips past the change and the caller reads a cache
// that will never contain it until something else triggers a sync. That is
// why a task created from the modal could vanish on the refetch right after
// it was created. Waiting for the in-flight run is not enough for the same
// reason, so callers that arrive mid-run share one follow-up run instead.
async sync() {
if (!isOfflineFeatureEnabled()) return false
if (this.isSyncing) return false
if (this.inFlight) {
if (!this.queued) {
this.queued = this.inFlight
.catch(() => false)
.then(() => {
this.queued = null
return this.sync()
})
}
return this.queued
}
this.inFlight = this._runSync().finally(() => {
this.inFlight = null
})
return this.inFlight
}
async _runSync() {
this.isSyncing = true
this._notify({ syncing: true, error: null })