Skip to content

Commit 8f1b7ce

Browse files
fix: resolve bugs found by testing suite
Co-authored-by: MasterMarcoHD <MasterMarcoHD@users.noreply.github.com>
1 parent 7d1b7ba commit 8f1b7ce

File tree

8 files changed

+337
-200
lines changed

8 files changed

+337
-200
lines changed

lib/src/cache/infra/services/default_cache_pipeline_service.dart

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,48 +31,74 @@ class DefaultCachePipelineService extends CachePipelineService {
3131
required CachePolicy<Serialized> policy,
3232
SerializationCodec<T, Serialized>? codec,
3333
}) async {
34-
CacheEntry<T>? fileEntry;
34+
final layers =
35+
<
36+
({
37+
CacheSource source,
38+
int priority,
39+
Future<CacheEntry<T>?> Function() read,
40+
Future<void> Function(CacheEntry<T> entry) write,
41+
})
42+
>[];
3543

3644
if (policy.useMemory && _memoryLayer != null) {
37-
try {
38-
final memoryEntry = await _memoryLayer.read<T, Serialized>(
39-
key,
40-
codec: codec,
41-
);
42-
if (memoryEntry != null) {
43-
return CacheResult<T>(
44-
value: memoryEntry.value,
45-
source: CacheSource.memory,
46-
isStale: false,
47-
);
48-
}
49-
} catch (e) {
50-
if (policy.strictLayerErrors) rethrow;
51-
log('Memory layer read failed for ${key.asStorageKey()}', e);
52-
}
45+
layers.add((
46+
source: CacheSource.memory,
47+
priority: _memoryLayer.priority,
48+
read: () => _memoryLayer.read<T, Serialized>(key, codec: codec),
49+
write: (entry) =>
50+
_memoryLayer.write<T, Serialized>(key, entry, codec: codec),
51+
));
5352
}
5453

5554
if (policy.useFile && _fileLayer != null) {
55+
layers.add((
56+
source: CacheSource.file,
57+
priority: _fileLayer.priority,
58+
read: () => _fileLayer.read<T, Serialized>(key, codec: codec),
59+
write: (entry) =>
60+
_fileLayer.write<T, Serialized>(key, entry, codec: codec),
61+
));
62+
}
63+
64+
layers.sort((a, b) => a.priority.compareTo(b.priority));
65+
66+
for (final layer in layers) {
67+
CacheEntry<T>? entry;
5668
try {
57-
fileEntry = await _fileLayer.read<T, Serialized>(key, codec: codec);
69+
entry = await layer.read();
5870
} catch (e) {
5971
if (policy.strictLayerErrors) rethrow;
60-
log('File layer read failed for ${key.asStorageKey()}', e);
72+
log(
73+
'${layer.source == CacheSource.memory ? 'Memory' : 'File'} layer read failed for ${key.asStorageKey()}',
74+
e,
75+
);
76+
continue;
6177
}
6278

63-
if (fileEntry != null) {
64-
if (policy.backfillHigherLayers &&
65-
policy.useMemory &&
66-
_memoryLayer != null) {
67-
await _memoryLayer.write<T, Serialized>(key, fileEntry, codec: codec);
79+
if (entry == null) continue;
80+
81+
if (policy.backfillHigherLayers) {
82+
for (final higherLayer in layers) {
83+
if (higherLayer.priority >= layer.priority) continue;
84+
85+
try {
86+
await higherLayer.write(entry);
87+
} catch (e) {
88+
if (policy.strictLayerErrors) rethrow;
89+
log(
90+
'${higherLayer.source == CacheSource.memory ? 'Memory' : 'File'} layer backfill failed for ${key.asStorageKey()}',
91+
e,
92+
);
93+
}
6894
}
69-
70-
return CacheResult<T>(
71-
value: fileEntry.value,
72-
source: CacheSource.file,
73-
isStale: fileEntry.isExpired,
74-
);
7595
}
96+
97+
return CacheResult<T>(
98+
value: entry.value,
99+
source: layer.source,
100+
isStale: layer.source == CacheSource.file ? entry.isExpired : false,
101+
);
76102
}
77103

78104
return null;

lib/src/module/module.dart

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ abstract class Module<RouteType, Config extends Object>
5050
final Set<LifecycleMixin> _activeInjectableSet = {};
5151
final Set<LifecycleMixin> _initializedInjectables = {};
5252

53+
/// `true` if the module is active and ready to serve its functionality.
54+
bool get isActive => _isActive;
55+
56+
/// `true` if the module is in the process of initializing.
57+
bool get isInitializing => _isInitializing;
58+
59+
/// `true` if the module is in the process of activating.
60+
bool get isActivating => _isActivating;
61+
62+
/// `true` if the module has been disposed and should no longer be used.
63+
bool get isDisposed => _disposed;
64+
65+
/// `true` if the module is active and not in the process of initializing or activating.
66+
bool get isReady => isActive && !isInitializing && !isActivating;
67+
5368
/// Override this getter to declare module dependencies.
5469
///
5570
/// Dependency mounting and activation are orchestrated by
@@ -135,11 +150,47 @@ abstract class Module<RouteType, Config extends Object>
135150
_activeRepos.add(repo);
136151
}
137152
}
153+
_isActive = true;
154+
} catch (e, s) {
155+
await _rollbackFailedActivation(e, s);
156+
rethrow;
138157
} finally {
139158
_isActivating = false;
140159
}
160+
}
161+
162+
Future<void> _rollbackFailedActivation(Object error, StackTrace stack) async {
163+
log(
164+
'Activation failed. Rolling back activated dependencies.',
165+
error,
166+
stack,
167+
);
168+
169+
for (final repo in _activeRepos.reversed) {
170+
try {
171+
await repo.deactivate();
172+
} catch (e, s) {
173+
log('Rollback deactivate failed for repo ${repo.runtimeType}', e, s);
174+
}
175+
}
176+
_activeRepos.clear();
177+
_activeRepoSet.clear();
178+
179+
for (final injectable in _activeInjectables.reversed) {
180+
try {
181+
await injectable.deactivate();
182+
} catch (e, s) {
183+
log(
184+
'Rollback deactivate failed for injectable ${injectable.runtimeType}',
185+
e,
186+
s,
187+
);
188+
}
189+
}
190+
_activeInjectables.clear();
191+
_activeInjectableSet.clear();
141192

142-
_isActive = true;
193+
_isActive = false;
143194
}
144195

145196
@mustCallSuper

lib/src/persistence/mixins/persistent_repo_state_mixin.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ mixin PersistentRepoStateMixin<T, Serialized extends Object>
7171
if (!persistencePolicy.enabled || !persistencePolicy.persistOnData) {
7272
return;
7373
}
74+
if (!persistencePolicy.saveNullData && value == null) {
75+
return;
76+
}
7477

7578
_persistDebounce?.cancel();
7679
_persistDebounce = Timer(persistencePolicy.persistDebounce, () async {
@@ -107,6 +110,10 @@ mixin PersistentRepoStateMixin<T, Serialized extends Object>
107110
}
108111

109112
Future<void> _saveSnapshot(T value) async {
113+
if (!persistencePolicy.saveNullData && value == null) {
114+
return;
115+
}
116+
110117
final now = DateTime.now();
111118
await RepoStatePersistenceService().save<T, Serialized>(
112119
snapshotKey,

lib/src/repo/mixins/use_repo_mixin.dart

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import 'package:grumpy/grumpy.dart';
99
mixin UseRepoMixin<D, E, L> on LifecycleMixin, LifecycleHooksMixin {
1010
final _subs = <StreamSubscription>[];
1111
final _watchedRepos = <Type, Repo>{};
12+
final _pendingRepoResolutions = <Type, Future<Repo<dynamic>>>{};
1213

1314
bool _installed = false;
14-
bool _handlingStateChange = false;
15+
int _stateChangeVersion = 0;
1516

1617
D? _lastData;
1718
E? _lastError;
@@ -21,22 +22,27 @@ mixin UseRepoMixin<D, E, L> on LifecycleMixin, LifecycleHooksMixin {
2122
log(
2223
'Detected state change in dependencies (${changedRepo.runtimeType}). Re-evaluating...',
2324
);
25+
final version = ++_stateChangeVersion;
26+
await _rebuildDependencyState(version);
27+
}
2428

25-
if (_handlingStateChange) return;
26-
_handlingStateChange = true;
27-
bool anyLoading = false;
29+
Future<void> _rebuildDependencyState(int version) async {
30+
var anyLoading = false;
2831
RepoErrorState? firstError;
2932

33+
D? nextData = _lastData;
34+
E? nextError = _lastError;
35+
L? nextLoading = _lastLoading;
36+
3037
for (final repo in _watchedRepos.values) {
3138
if (repo.state.hasError) {
3239
log(
3340
'Dependency of type ${repo.runtimeType} has error. Rebuilding error state...',
3441
);
35-
final errorState = repo.state.asError;
36-
37-
firstError = errorState;
42+
firstError = repo.state.asError;
3843
break;
39-
} else if (repo.state.isLoading) {
44+
}
45+
if (repo.state.isLoading) {
4046
log(
4147
'Dependency of type ${repo.runtimeType} is loading. Rebuilding loading state...',
4248
);
@@ -46,32 +52,49 @@ mixin UseRepoMixin<D, E, L> on LifecycleMixin, LifecycleHooksMixin {
4652

4753
final allDataReady = !anyLoading && firstError == null;
4854

49-
_lastError = firstError != null
50-
? await onDependencyError(firstError.error, firstError.stackTrace)
51-
: null;
52-
_lastLoading = anyLoading ? onDependenciesLoading() : null;
53-
5455
try {
55-
if (allDataReady) log('All dependencies are ready. Rebuilding data...');
56-
_lastData = allDataReady ? await onDependenciesReady() : null;
57-
if (allDataReady) log('Dependencies ready, obtained new data.');
56+
if (firstError != null) {
57+
nextError = await onDependencyError(
58+
firstError.error,
59+
firstError.stackTrace,
60+
);
61+
nextLoading = null;
62+
nextData = null;
63+
} else if (anyLoading) {
64+
nextError = null;
65+
nextLoading = onDependenciesLoading();
66+
nextData = null;
67+
} else if (allDataReady) {
68+
log('All dependencies are ready. Rebuilding data...');
69+
nextError = null;
70+
nextLoading = null;
71+
nextData = await onDependenciesReady();
72+
log('Dependencies ready, obtained new data.');
73+
}
5874
} on NoRepoDataError catch (e, st) {
5975
if (e.state.isLoading) {
60-
_lastLoading = onDependenciesLoading();
61-
_lastData = null;
76+
nextError = null;
77+
nextLoading = onDependenciesLoading();
78+
nextData = null;
6279
} else {
63-
_lastError = await onDependencyError(e, st);
64-
_lastData = null;
80+
nextError = await onDependencyError(e, st);
81+
nextLoading = null;
82+
nextData = null;
6583
}
6684
} catch (e, st) {
67-
_lastError = await onDependencyError(e, st);
68-
_lastData = null;
85+
nextError = await onDependencyError(e, st);
86+
nextLoading = null;
87+
nextData = null;
6988
}
7089

90+
if (version != _stateChangeVersion) return;
91+
92+
_lastData = nextData;
93+
_lastError = nextError;
94+
_lastLoading = nextLoading;
95+
7196
log('State rebuilt, notifying listeners...');
7297
await dependenciesChanged();
73-
74-
_handlingStateChange = false;
7598
}
7699

77100
Future<void> _discover() async {
@@ -142,17 +165,41 @@ mixin UseRepoMixin<D, E, L> on LifecycleMixin, LifecycleHooksMixin {
142165
return (repo.state.requireData, repo);
143166
}
144167

145-
final repo = await GetIt.I.getAsync<R>();
168+
final pending = _pendingRepoResolutions[R];
169+
if (pending != null) {
170+
final repo = await pending as R;
171+
return (repo.state.requireData, repo);
172+
}
146173

147-
log('Discovered new dependency. Now watching repo of type $R');
174+
final resolveAndWatch = (() async {
175+
final repo = await GetIt.I.getAsync<R>();
176+
if (_watchedRepos.containsKey(R)) {
177+
return _watchedRepos[R] as R;
178+
}
148179

149-
_watchedRepos[R] = repo;
180+
log('Discovered new dependency. Now watching repo of type $R');
181+
_watchedRepos[R] = repo;
150182

151-
final sub = repo.stream.listen((_) async {
152-
await _onWatchedRepoStateChange(repo);
153-
});
183+
var ignoredInitialReplay = false;
184+
final sub = repo.stream.listen((_) async {
185+
if (!ignoredInitialReplay) {
186+
ignoredInitialReplay = true;
187+
return;
188+
}
189+
await _onWatchedRepoStateChange(repo);
190+
});
191+
192+
_subs.add(sub);
193+
return repo;
194+
})();
154195

155-
_subs.add(sub);
196+
_pendingRepoResolutions[R] = resolveAndWatch;
197+
late final R repo;
198+
try {
199+
repo = await resolveAndWatch;
200+
} finally {
201+
await _pendingRepoResolutions.remove(R);
202+
}
156203

157204
return (repo.state.requireData, repo);
158205
}

lib/src/routing/infra/services/routing_kit_routing_service.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ class RoutingKitRoutingService<T, Config extends Object>
4343
RouteContext? get currentContext => _context;
4444

4545
@override
46-
FutureOr<void> free() {
47-
super.free();
46+
FutureOr<void> free() async {
47+
await super.free();
4848
_listeners.clear();
49+
_pendingNavigations.clear();
50+
if (!_viewChangeController.isClosed) {
51+
await _viewChangeController.close();
52+
}
4953
}
5054

5155
@override
@@ -81,8 +85,8 @@ class RoutingKitRoutingService<T, Config extends Object>
8185
_context = null;
8286
_listeners.clear();
8387
_moduleCache.clear();
88+
_pendingNavigations.clear();
8489
await moduleRegistry.sync(<Module<T, Config>>[]);
85-
await _viewChangeController.close();
8690
}
8791

8892
@override
@@ -359,7 +363,7 @@ class RoutingKitRoutingService<T, Config extends Object>
359363

360364
@override
361365
Stream<ViewChangedEvent<T, Config>> get viewStream =>
362-
_viewChangeController.stream.asBroadcastStream();
366+
_viewChangeController.stream;
363367

364368
@override
365369
Future<void> get currentNavigation => _currentNavigation;

0 commit comments

Comments
 (0)