data_loader 1.0.0
This commit is contained in:
parent
3ac089424b
commit
2105f93f95
11 changed files with 268 additions and 0 deletions
|
@ -25,6 +25,7 @@ This mono repo is split into several sub-projects,
|
|||
each with its own detailed documentation and examples:
|
||||
* `angel_graphql` - Support for handling GraphQL via HTTP and
|
||||
WebSockets in the [Angel](https://angel-dart.dev) framework. Also serves as the `package:graphql_server` reference implementation.
|
||||
* `data_loader` - A Dart port of [`graphql/data_loader`](https://github.com/graphql/dataloader).
|
||||
* `example_star_wars`: An example GraphQL API built using
|
||||
`package:angel_graphql`.
|
||||
* `graphql_generator`: Generates `package:graphql_schema` object types from concrete Dart classes.
|
||||
|
|
3
data_loader/.gitignore
vendored
Normal file
3
data_loader/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.packages
|
||||
pubspec.lock
|
||||
.dart_tool
|
2
data_loader/CHANGELOG.md
Normal file
2
data_loader/CHANGELOG.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# 1.0.0
|
||||
* Initial version.
|
21
data_loader/LICENSE
Normal file
21
data_loader/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 The Angel Framework
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
22
data_loader/README.md
Normal file
22
data_loader/README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# data_loader
|
||||
Batch and cache database lookups. Works well with GraphQL.
|
||||
Ported from the original JS version:
|
||||
https://github.com/graphql/dataloader
|
||||
|
||||
## Installation
|
||||
In your pubspec.yaml:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
data_loader: ^1.0.0
|
||||
```
|
||||
|
||||
## Usage
|
||||
Complete example:
|
||||
https://github.com/angel-dart/graphql/blob/master/data_loader/example/main.dart
|
||||
|
||||
```dart
|
||||
var userLoader = new DataLoader((key) => myBatchGetUsers(keys));
|
||||
var invitedBy = await userLoader.load(1)then(user => userLoader.load(user.invitedByID))
|
||||
print('User 1 was invited by $invitedBy'));
|
||||
```
|
4
data_loader/analysis_options.yaml
Normal file
4
data_loader/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
44
data_loader/example/main.dart
Normal file
44
data_loader/example/main.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
import 'dart:async';
|
||||
import 'package:data_loader/data_loader.dart';
|
||||
import 'package:graphql_schema/graphql_schema.dart';
|
||||
|
||||
external Future<List<Todo>> fetchTodos(Iterable<int> ids);
|
||||
|
||||
main() async {
|
||||
// Create a DataLoader. By default, it caches lookups.
|
||||
var todoLoader = DataLoader(fetchTodos); // DataLoader<int, Todo>
|
||||
|
||||
// type Todo { id: Int, text: String, is_complete: Boolean }
|
||||
var todoType = objectType(
|
||||
'Todo',
|
||||
fields: [
|
||||
field('id', graphQLInt),
|
||||
field('text', graphQLString),
|
||||
field('is_complete', graphQLBoolean),
|
||||
],
|
||||
);
|
||||
|
||||
// type Query { todo($id: Int!) Todo }
|
||||
// ignore: unused_local_variable
|
||||
var schema = graphQLSchema(
|
||||
queryType: objectType(
|
||||
'Query',
|
||||
fields: [
|
||||
field(
|
||||
'todo',
|
||||
listOf(todoType),
|
||||
inputs: [GraphQLFieldInput('id', graphQLInt.nonNullable())],
|
||||
resolve: (_, args) => todoLoader.load(args['id'] as int),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Do something with your schema...
|
||||
}
|
||||
|
||||
abstract class Todo {
|
||||
int get id;
|
||||
String get text;
|
||||
bool get isComplete;
|
||||
}
|
85
data_loader/lib/data_loader.dart
Normal file
85
data_loader/lib/data_loader.dart
Normal file
|
@ -0,0 +1,85 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
/// A utility for batching multiple requests together, to improve application performance.
|
||||
///
|
||||
/// Enqueues batches of requests until the next tick, when they are processed in bulk.
|
||||
///
|
||||
/// Port of Facebook's `DataLoader`:
|
||||
/// https://github.com/graphql/dataloader
|
||||
class DataLoader<Id, Data> {
|
||||
/// Invoked to fetch a batch of keys simultaneously.
|
||||
final FutureOr<Iterable<Data>> Function(Iterable<Id>) loadMany;
|
||||
|
||||
/// Whether to use a memoization cache to store the results of past lookups.
|
||||
final bool cache;
|
||||
|
||||
var _cache = <Id, Data>{};
|
||||
var _queue = Queue<_QueueItem<Id, Data>>();
|
||||
bool _started = false;
|
||||
|
||||
DataLoader(this.loadMany, {this.cache = true});
|
||||
|
||||
Future<void> _onTick() async {
|
||||
if (_queue.isNotEmpty) {
|
||||
var current = _queue.toList();
|
||||
_queue.clear();
|
||||
|
||||
var data = await loadMany(current.map((i) => i.id));
|
||||
|
||||
for (int i = 0; i < current.length; i++) {
|
||||
var item = current[i];
|
||||
var value = data.elementAt(i);
|
||||
if (cache) _cache[item.id] = value;
|
||||
item.completer.complete(value);
|
||||
}
|
||||
}
|
||||
|
||||
_started = false;
|
||||
// if (!_closed) scheduleMicrotask(_onTick);
|
||||
}
|
||||
|
||||
/// Clears the value at [key], if it exists.
|
||||
void clear(Id key) => _cache.remove(key);
|
||||
|
||||
/// Clears the entire cache.
|
||||
void clearAll() => _cache.clear();
|
||||
|
||||
/// Primes the cache with the provided key and value. If the key already exists, no change is made.
|
||||
///
|
||||
/// To forcefully prime the cache, clear the key first with
|
||||
/// `loader..clear(key)..prime(key, value)`.
|
||||
void prime(Id key, Data value) => _cache.putIfAbsent(key, () => value);
|
||||
|
||||
/// Closes this [DataLoader], cancelling all pending requests.
|
||||
void close() {
|
||||
while (_queue.isNotEmpty) {
|
||||
_queue.removeFirst().completer.completeError(
|
||||
StateError('The DataLoader was closed before the item was loaded.'));
|
||||
}
|
||||
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
/// Returns a [Future] that completes when the next batch of requests completes.
|
||||
Future<Data> load(Id id) {
|
||||
if (cache && _cache.containsKey(id)) {
|
||||
return Future<Data>.value(_cache[id]);
|
||||
} else {
|
||||
var item = _QueueItem<Id, Data>(id);
|
||||
_queue.add(item);
|
||||
if (!_started) {
|
||||
_started = true;
|
||||
scheduleMicrotask(_onTick);
|
||||
}
|
||||
return item.completer.future;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _QueueItem<Id, Data> {
|
||||
final Id id;
|
||||
final Completer<Data> completer = Completer();
|
||||
|
||||
_QueueItem(this.id);
|
||||
}
|
0
data_loader/mono_pkg.yaml
Normal file
0
data_loader/mono_pkg.yaml
Normal file
11
data_loader/pubspec.yaml
Normal file
11
data_loader/pubspec.yaml
Normal file
|
@ -0,0 +1,11 @@
|
|||
name: data_loader
|
||||
version: 1.0.0
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
description: Batch and cache database lookups. Works well with GraphQL. Ported from JS.
|
||||
homepage: https://github.com/angel-dart/graphql
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <3.0.0"
|
||||
dev_dependencies:
|
||||
graphql_schema: ^1.0.0
|
||||
pedantic: ^1.0.0
|
||||
test: ">=0.12.0 <2.0.0"
|
75
data_loader/test/all_test.dart
Normal file
75
data_loader/test/all_test.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'dart:async';
|
||||
import 'package:data_loader/data_loader.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var numbers = List.generate(10, (i) => i.toStringAsFixed(2));
|
||||
var numberLoader = DataLoader<int, String>((ids) {
|
||||
print('ID batch: $ids');
|
||||
return ids.map((i) => numbers[i]);
|
||||
});
|
||||
|
||||
test('batch', () async {
|
||||
var zero = numberLoader.load(0);
|
||||
var one = numberLoader.load(1);
|
||||
var two = numberLoader.load(2);
|
||||
var batch = await Future.wait([zero, one, two]);
|
||||
print('Fetched result: $batch');
|
||||
expect(batch, ['0.00', '1.00', '2.00']);
|
||||
});
|
||||
|
||||
group('cache', () {
|
||||
DataLoader<int, _Unique> uniqueLoader, noCache;
|
||||
|
||||
setUp(() {
|
||||
uniqueLoader = DataLoader<int, _Unique>((ids) async {
|
||||
var numbers = await numberLoader.loadMany(ids);
|
||||
return numbers.map((s) => _Unique(s));
|
||||
});
|
||||
noCache = DataLoader(uniqueLoader.loadMany, cache: false);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
uniqueLoader.close();
|
||||
noCache.close();
|
||||
});
|
||||
|
||||
test('only lookup once', () async {
|
||||
var a = await uniqueLoader.load(3);
|
||||
var b = await uniqueLoader.load(3);
|
||||
expect(a, b);
|
||||
});
|
||||
|
||||
test('can be disabled', () async {
|
||||
var a = await noCache.load(3);
|
||||
var b = await noCache.load(3);
|
||||
expect(a, isNot(b));
|
||||
});
|
||||
|
||||
test('clear', () async {
|
||||
var a = await uniqueLoader.load(3);
|
||||
uniqueLoader.clear(3);
|
||||
var b = await uniqueLoader.load(3);
|
||||
expect(a, isNot(b));
|
||||
});
|
||||
|
||||
test('clearAll', () async {
|
||||
var a = await uniqueLoader.load(3);
|
||||
uniqueLoader.clearAll();
|
||||
var b = await uniqueLoader.load(3);
|
||||
expect(a, isNot(b));
|
||||
});
|
||||
|
||||
test('prime', () async {
|
||||
uniqueLoader.prime(3, _Unique('hey'));
|
||||
var a = await uniqueLoader.load(3);
|
||||
expect(a.value, 'hey');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _Unique {
|
||||
final String value;
|
||||
|
||||
_Unique(this.value);
|
||||
}
|
Loading…
Reference in a new issue