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:
|
each with its own detailed documentation and examples:
|
||||||
* `angel_graphql` - Support for handling GraphQL via HTTP and
|
* `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.
|
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
|
* `example_star_wars`: An example GraphQL API built using
|
||||||
`package:angel_graphql`.
|
`package:angel_graphql`.
|
||||||
* `graphql_generator`: Generates `package:graphql_schema` object types from concrete Dart classes.
|
* `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