Skip to content

Instantly share code, notes, and snippets.

@nrbnlulu
Created March 29, 2026 15:38
Show Gist options
  • Select an option

  • Save nrbnlulu/96b776e50b27d71bb6c0123a8a2bea58 to your computer and use it in GitHub Desktop.

Select an option

Save nrbnlulu/96b776e50b27d71bb6c0123a8a2bea58 to your computer and use it in GitHub Desktop.
dart dataloader
import 'dart:async';
typedef LoaderFn<K, T, E> = Future<Map<K, Result<T, E>>> Function(List<K> keys);
class DataloaderConfig {
const DataloaderConfig({
this.maxBatchSize = 100,
this.delayBeforeResolve = const Duration(milliseconds: 100),
});
final int maxBatchSize;
final Duration delayBeforeResolve;
}
class Dataloader<K, T, E extends Object> {
Dataloader({
required LoaderFn<K, T, E> loaderFn,
DataloaderConfig config = const DataloaderConfig(),
}) : _loaderFn = loaderFn,
_config = config;
final LoaderFn<K, T, E> _loaderFn;
final DataloaderConfig _config;
final Map<K, Completer<Result<T, Object>>> _unresolvedFutures = {};
final Map<K, Result<T, Object>> _cache = {};
Timer? _resolveSchedulerTimer;
static Result<T, Object> _liftErr<T, E>(Result<T, E> r) => switch (r) {
Ok<T, E>(:final data) => Ok<T, Object>(data),
Err<T, E>(:final error) => Err<T, Object>(error!),
};
Future<void> _resolveBatch() async {
final keys = List<K>.from(_unresolvedFutures.keys);
if (keys.isEmpty) return;
try {
final results = await _loaderFn(keys);
for (final entry in results.entries) {
final result = _liftErr(entry.value);
_cache[entry.key] = result;
_unresolvedFutures.remove(entry.key)?.complete(result);
}
for (final key in List<K>.from(_unresolvedFutures.keys)) {
_unresolvedFutures.remove(key)?.complete(const Err('not found'));
}
} catch (e) {
for (final key in keys) {
_unresolvedFutures.remove(key)?.completeError(e);
}
}
}
void _resetResolveScheduler() {
_resolveSchedulerTimer?.cancel();
_resolveSchedulerTimer = Timer(_config.delayBeforeResolve, _resolveBatch);
}
Future<Result<T, Object>> _load(K key) {
final cached = _cache[key];
if (cached != null) return Future.value(cached);
if (_unresolvedFutures.containsKey(key)) {
_resetResolveScheduler();
return _unresolvedFutures[key]!.future;
}
final completer = Completer<Result<T, Object>>();
_unresolvedFutures[key] = completer;
if (_unresolvedFutures.length > _config.maxBatchSize) {
_resolveSchedulerTimer?.cancel();
_resolveBatch();
} else {
_resetResolveScheduler();
}
return completer.future;
}
Future<T?> loadOpt(K? key) async {
if (key == null) return null;
try {
return (await _load(key)).ok;
} catch (_) {
return null;
}
}
Future<T> load(K key) async {
return switch (await _load(key)) {
Ok<T, Object>(:final data) => data,
Err<T, Object>(:final error) => throw StateError('Dataloader key not found: $error'),
};
}
void clearCache(K key) => _cache.remove(key);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment