data_loader 1.0.0

This commit is contained in:
Tobe O 2019-04-18 23:20:18 -04:00
parent 3ac089424b
commit 2105f93f95
11 changed files with 268 additions and 0 deletions

View file

@ -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
View file

@ -0,0 +1,3 @@
.packages
pubspec.lock
.dart_tool

2
data_loader/CHANGELOG.md Normal file
View file

@ -0,0 +1,2 @@
# 1.0.0
* Initial version.

21
data_loader/LICENSE Normal file
View 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
View 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'));
```

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View 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;
}

View 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);
}

View file

11
data_loader/pubspec.yaml Normal file
View 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"

View 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);
}