Skip to content

Commit ab59fc0

Browse files
committed
fix: prevent double notifications in getWithMeta flow
1 parent 6145e29 commit ab59fc0

2 files changed

Lines changed: 30 additions & 16 deletions

File tree

packages/syncache/lib/src/syncache.dart

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ class Syncache<T> {
272272
retry: retry ?? defaultRetry,
273273
cancel: cancel,
274274
tags: null,
275+
skipNotify: true, // Meta strategy handles its own notifications
275276
);
276277
}
277278

@@ -1399,6 +1400,11 @@ class Syncache<T> {
13991400
///
14001401
/// This unified method handles all policy variants,
14011402
/// delegating fetch/wrap operations to the strategy.
1403+
///
1404+
/// When [skipNotify] is true, the method will not call [_notify] after
1405+
/// cache hits or fetches. This is used by [getWithMeta] since the meta
1406+
/// strategy handles value notifications internally via [_notifyValue],
1407+
/// and [watchWithMeta] handles meta notifications via manual emission.
14021408
Future<R> _executePolicy<R>({
14031409
required String key,
14041410
required Policy policy,
@@ -1407,14 +1413,15 @@ class Syncache<T> {
14071413
required RetryConfig retry,
14081414
required CancellationToken? cancel,
14091415
required List<String>? tags,
1416+
bool skipNotify = false,
14101417
}) async {
14111418
switch (policy) {
14121419
case Policy.cacheOnly:
14131420
final cached = await store.read(key);
14141421
if (cached == null) {
14151422
throw CacheMissException(key);
14161423
}
1417-
_notify(key, isFromCache: true);
1424+
if (!skipNotify) _notify(key, isFromCache: true);
14181425
return strategy.wrapCached(cached, isStale: cached.meta.isExpired);
14191426

14201427
case Policy.networkOnly:
@@ -1426,7 +1433,7 @@ class Syncache<T> {
14261433
cancel,
14271434
tags,
14281435
);
1429-
_notify(key, isFromCache: false);
1436+
if (!skipNotify) _notify(key, isFromCache: false);
14301437
return result;
14311438

14321439
case Policy.refresh:
@@ -1439,21 +1446,21 @@ class Syncache<T> {
14391446
cancel,
14401447
tags,
14411448
);
1442-
_notify(key, isFromCache: false);
1449+
if (!skipNotify) _notify(key, isFromCache: false);
14431450
return result;
14441451
}
14451452
final cached = await store.read(key);
14461453
if (cached == null) {
14471454
throw CacheMissException(key);
14481455
}
1449-
_notify(key, isFromCache: true);
1456+
if (!skipNotify) _notify(key, isFromCache: true);
14501457
return strategy.wrapCached(cached, isStale: cached.meta.isExpired);
14511458

14521459
case Policy.offlineFirst:
14531460
final cached = await store.read(key);
14541461
if (cached != null && !cached.meta.isExpired) {
14551462
_notifyObservers((o) => o.onCacheHit(key));
1456-
_notify(key, isFromCache: true);
1463+
if (!skipNotify) _notify(key, isFromCache: true);
14571464
return strategy.wrapCached(cached, isStale: false);
14581465
}
14591466

@@ -1469,7 +1476,7 @@ class Syncache<T> {
14691476
cancel,
14701477
tags,
14711478
);
1472-
_notify(key, isFromCache: false);
1479+
if (!skipNotify) _notify(key, isFromCache: false);
14731480
return result;
14741481
} on CancelledException {
14751482
rethrow;
@@ -1478,7 +1485,7 @@ class Syncache<T> {
14781485
throw CacheMissException(key);
14791486
}
14801487
if (cached != null) {
1481-
_notify(key, isFromCache: true);
1488+
if (!skipNotify) _notify(key, isFromCache: true);
14821489
return strategy.wrapCached(cached,
14831490
isStale: cached.meta.isExpired);
14841491
}
@@ -1487,7 +1494,7 @@ class Syncache<T> {
14871494
}
14881495

14891496
if (cached != null) {
1490-
_notify(key, isFromCache: true);
1497+
if (!skipNotify) _notify(key, isFromCache: true);
14911498
return strategy.wrapCached(cached, isStale: cached.meta.isExpired);
14921499
}
14931500

@@ -1502,6 +1509,7 @@ class Syncache<T> {
15021509
cancel: cancel,
15031510
tags: tags,
15041511
refreshOnlyIfExpired: true,
1512+
skipNotify: skipNotify,
15051513
);
15061514

15071515
case Policy.cacheAndRefresh:
@@ -1513,6 +1521,7 @@ class Syncache<T> {
15131521
cancel: cancel,
15141522
tags: tags,
15151523
refreshOnlyIfExpired: false,
1524+
skipNotify: skipNotify,
15161525
);
15171526
}
15181527
}
@@ -1530,6 +1539,7 @@ class Syncache<T> {
15301539
required CancellationToken? cancel,
15311540
required List<String>? tags,
15321541
required bool refreshOnlyIfExpired,
1542+
bool skipNotify = false,
15331543
}) async {
15341544
final cached = await store.read(key);
15351545

@@ -1542,13 +1552,14 @@ class Syncache<T> {
15421552
if (shouldRefresh) {
15431553
strategy
15441554
.backgroundRefresh(_fetchEngine, key, ttl, retry, tags)
1545-
.then((_) => _notify(key, isFromCache: false))
1546-
.catchError((Object e, StackTrace st) {
1555+
.then((_) {
1556+
_notify(key, isFromCache: false);
1557+
}).catchError((Object e, StackTrace st) {
15471558
_notifyObservers((o) => o.onFetchError(key, e, st));
15481559
});
15491560
}
15501561

1551-
_notify(key, isFromCache: true);
1562+
if (!skipNotify) _notify(key, isFromCache: true);
15521563
return strategy.wrapCached(cached, isStale: cached.meta.isExpired);
15531564
}
15541565

@@ -1563,7 +1574,7 @@ class Syncache<T> {
15631574
cancel,
15641575
tags,
15651576
);
1566-
_notify(key, isFromCache: false);
1577+
if (!skipNotify) _notify(key, isFromCache: false);
15671578
return result;
15681579
}
15691580

packages/syncache/test/cache_and_refresh_test.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,20 +425,23 @@ void main() {
425425
}
426426

427427
// Start concurrent requests
428-
final result1 = await cache.get(
428+
final future1 = cache.get(
429429
key: 'dedup-car-key',
430430
fetch: fetcher,
431431
policy: Policy.cacheAndRefresh,
432432
);
433-
final result2 = await cache.get(
433+
final future2 = cache.get(
434434
key: 'dedup-car-key',
435435
fetch: fetcher,
436436
policy: Policy.cacheAndRefresh,
437437
);
438438

439+
// Wait for both to complete
440+
final results = await Future.wait([future1, future2]);
441+
439442
// Both should return cached value immediately
440-
expect(result1, equals('cached-value'));
441-
expect(result2, equals('cached-value'));
443+
expect(results[0], equals('cached-value'));
444+
expect(results[1], equals('cached-value'));
442445

443446
// Complete background refresh
444447
completer.complete('fresh-value');

0 commit comments

Comments
 (0)