Add 'packages/poll/' from commit '14637ac137e083a90e1db397f0a90561888c3bde'
git-subtree-dir: packages/poll git-subtree-mainline:a5f5661650
git-subtree-split:14637ac137
This commit is contained in:
commit
aa092a4111
17 changed files with 580 additions and 0 deletions
64
packages/poll/.gitignore
vendored
Normal file
64
packages/poll/.gitignore
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.xml
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# CMake
|
||||
cmake-build-debug/
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
6
packages/poll/.idea/misc.xml
Normal file
6
packages/poll/.idea/misc.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
8
packages/poll/.idea/modules.xml
Normal file
8
packages/poll/.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/poll.iml" filepath="$PROJECT_DIR$/poll.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,8 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="fires modified in all_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/test/all_test.dart" />
|
||||
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
|
||||
<option name="testName" value="fires modified" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,8 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="fires removed in all_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/test/all_test.dart" />
|
||||
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
|
||||
<option name="testName" value="fires removed" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,6 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="tests in all_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/test/all_test.dart" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
6
packages/poll/.idea/vcs.xml
Normal file
6
packages/poll/.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
1
packages/poll/.travis.yml
Normal file
1
packages/poll/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
2
packages/poll/CHANGELOG.md
Normal file
2
packages/poll/CHANGELOG.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# 1.0.0
|
||||
* Created package + tests
|
21
packages/poll/LICENSE
Normal file
21
packages/poll/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.
|
42
packages/poll/README.md
Normal file
42
packages/poll/README.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# poll
|
||||
[](https://pub.dartlang.org/packages/angel_poll)
|
||||
[](https://travis-ci.org/angel-dart/poll)
|
||||
|
||||
`package:angel_client` support for "realtime" interactions with Angel via long polling.
|
||||
|
||||
Angel supports [WebSockets](https://github.com/angel-dart/websocket) on the server and client, which
|
||||
makes it very straightforward to implement realtime collections. However, not every user's browser
|
||||
supports WebSockets. In such a case, applications might *gracefully degrade* to long-polling
|
||||
the server for changes.
|
||||
|
||||
A `PollingService` wraps a client-side `Service` (typically a REST-based one), and calls its
|
||||
`index` method at a regular interval. After indexing, the `PollingService` performs a diff
|
||||
and identifies whether items have been created, modified, or removed. The updates are sent out
|
||||
through `onCreated`, `onModified`, etc., effectively managing a real-time collection of data.
|
||||
|
||||
A common use-case would be passing this service to `ServiceList`, a class that manages the state
|
||||
of a collection managed in real-time.
|
||||
|
||||
```dart
|
||||
import 'package:angel_client/io.dart';
|
||||
import 'package:angel_poll/angel_poll.dart';
|
||||
|
||||
main() {
|
||||
var app = new Rest('http://localhost:3000');
|
||||
|
||||
var todos = new ServiceList(
|
||||
new PollingService(
|
||||
// Typically, you'll pass a REST-based service instance here.
|
||||
app.service('api/todos'),
|
||||
|
||||
// `index` called every 5 seconds
|
||||
const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
|
||||
todos.onChange.listen((_) {
|
||||
// Something happened here.
|
||||
// Maybe an item was created, modified, etc.
|
||||
});
|
||||
}
|
||||
```
|
2
packages/poll/analysis_options.yaml
Normal file
2
packages/poll/analysis_options.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
21
packages/poll/example/main.dart
Normal file
21
packages/poll/example/main.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'package:angel_client/io.dart';
|
||||
import 'package:angel_poll/angel_poll.dart';
|
||||
|
||||
main() {
|
||||
var app = new Rest('http://localhost:3000');
|
||||
|
||||
var todos = new ServiceList(
|
||||
new PollingService(
|
||||
// Typically, you'll pass a REST-based service instance here.
|
||||
app.service('api/todos'),
|
||||
|
||||
// `index` called every 5 seconds
|
||||
const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
|
||||
todos.onChange.listen((_) {
|
||||
// Something happened here.
|
||||
// Maybe an item was created, modified, etc.
|
||||
});
|
||||
}
|
252
packages/poll/lib/angel_poll.dart
Normal file
252
packages/poll/lib/angel_poll.dart
Normal file
|
@ -0,0 +1,252 @@
|
|||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:angel_client/angel_client.dart';
|
||||
|
||||
/// A [Service] that facilitates real-time updates via the long polling of an [inner] service.
|
||||
///
|
||||
/// Works well with [ServiceList].
|
||||
class PollingService extends Service {
|
||||
/// The underlying [Service] that does the actual communication with the server.
|
||||
final Service inner;
|
||||
|
||||
/// Perform computations after polling to discern whether new items were created.
|
||||
final bool checkForCreated;
|
||||
|
||||
/// Perform computations after polling to discern whether items were modified.
|
||||
final bool checkForModified;
|
||||
|
||||
/// Perform computations after polling to discern whether items were removed.
|
||||
final bool checkForRemoved;
|
||||
|
||||
/// An [EqualityBy] used to compare the ID's of two items.
|
||||
///
|
||||
/// Defaults to comparing the [idField] of two `Map` instances.
|
||||
final EqualityBy compareId;
|
||||
|
||||
/// An [Equality] used to discern whether two items, with the same [idField], are the same item.
|
||||
///
|
||||
/// Defaults to [MapEquality], which deep-compares `Map` instances.
|
||||
final Equality compareItems;
|
||||
|
||||
/// A [String] used as an index through which to compare `Map` instances.
|
||||
///
|
||||
/// Defaults to `id`.
|
||||
final String idField;
|
||||
|
||||
/// If `true` (default: `false`), then `index` events will be handled as a [Map] containing a `data` field.
|
||||
///
|
||||
/// See https://github.com/angel-dart/paginate.
|
||||
final bool asPaginated;
|
||||
|
||||
final List _items = [];
|
||||
final List<StreamSubscription> _subs = [];
|
||||
|
||||
final StreamController _onIndexed = new StreamController(),
|
||||
_onRead = new StreamController(),
|
||||
_onCreated = new StreamController(),
|
||||
_onModified = new StreamController(),
|
||||
_onUpdated = new StreamController(),
|
||||
_onRemoved = new StreamController();
|
||||
|
||||
Timer _timer;
|
||||
|
||||
@override
|
||||
Angel get app => inner.app;
|
||||
|
||||
@override
|
||||
Stream get onIndexed => _onIndexed.stream;
|
||||
|
||||
@override
|
||||
Stream get onRead => _onRead.stream;
|
||||
|
||||
@override
|
||||
Stream get onCreated => _onCreated.stream;
|
||||
|
||||
@override
|
||||
Stream get onModified => _onModified.stream;
|
||||
|
||||
@override
|
||||
Stream get onUpdated => _onUpdated.stream;
|
||||
|
||||
@override
|
||||
Stream get onRemoved => _onRemoved.stream;
|
||||
|
||||
PollingService(this.inner, Duration interval,
|
||||
{this.checkForCreated: true,
|
||||
this.checkForModified: true,
|
||||
this.checkForRemoved: true,
|
||||
this.idField: 'id',
|
||||
this.asPaginated: false,
|
||||
EqualityBy compareId,
|
||||
this.compareItems: const MapEquality()})
|
||||
: compareId = compareId ?? new EqualityBy((map) => map[idField ?? 'id']) {
|
||||
_timer = new Timer.periodic(interval, (_) {
|
||||
index().catchError(_onIndexed.addError);
|
||||
});
|
||||
|
||||
var streams = <Stream, StreamController>{
|
||||
inner.onRead: _onRead,
|
||||
inner.onCreated: _onCreated,
|
||||
inner.onModified: _onModified,
|
||||
inner.onUpdated: _onUpdated,
|
||||
inner.onRemoved: _onRemoved,
|
||||
};
|
||||
|
||||
streams.forEach((stream, ctrl) {
|
||||
_subs.add(stream.listen(ctrl.add, onError: ctrl.addError));
|
||||
});
|
||||
|
||||
_subs.add(
|
||||
inner.onIndexed.listen(
|
||||
_handleIndexed,
|
||||
onError: _onIndexed.addError,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() async {
|
||||
_timer.cancel();
|
||||
_subs.forEach((s) => s.cancel());
|
||||
_onIndexed.close();
|
||||
_onRead.close();
|
||||
_onCreated.close();
|
||||
_onModified.close();
|
||||
_onUpdated.close();
|
||||
_onRemoved.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future index([Map params]) {
|
||||
return inner.index().then((data) {
|
||||
return asPaginated == true ? data['data'] : data;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future remove(id, [Map params]) {
|
||||
return inner.remove(id, params).then((result) {
|
||||
_items.remove(result);
|
||||
return result;
|
||||
}).catchError(_onRemoved.addError);
|
||||
}
|
||||
|
||||
_handleUpdate(result) {
|
||||
int index = -1;
|
||||
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
if (compareId.equals(_items[i], result)) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index > -1) {
|
||||
_items[index] = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future update(id, data, [Map params]) {
|
||||
return inner
|
||||
.update(id, data, params)
|
||||
.then(_handleUpdate)
|
||||
.catchError(_onUpdated.addError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future modify(id, data, [Map params]) {
|
||||
return inner
|
||||
.modify(id, data, params)
|
||||
.then(_handleUpdate)
|
||||
.catchError(_onModified.addError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future create(data, [Map params]) {
|
||||
return inner.create(data, params).then((result) {
|
||||
_items.add(result);
|
||||
return result;
|
||||
}).catchError(_onCreated.addError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future read(id, [Map params]) {
|
||||
return inner.read(id, params);
|
||||
}
|
||||
|
||||
void _handleIndexed(data) {
|
||||
var items = asPaginated == true ? data['data'] : data;
|
||||
bool changesComputed = false;
|
||||
|
||||
if (checkForCreated != false) {
|
||||
var newItems = <int, dynamic>{};
|
||||
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
|
||||
if (!_items.any((i) => compareId.equals(i, item))) {
|
||||
newItems[i] = item;
|
||||
}
|
||||
}
|
||||
|
||||
newItems.forEach((index, item) {
|
||||
_items.insert(index, item);
|
||||
_onCreated.add(item);
|
||||
});
|
||||
|
||||
changesComputed = newItems.isNotEmpty;
|
||||
}
|
||||
|
||||
if (checkForRemoved != false) {
|
||||
var removedItems = <int, dynamic>{};
|
||||
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
var item = _items[i];
|
||||
|
||||
if (!items.any((i) => compareId.equals(i, item))) {
|
||||
removedItems[i] = item;
|
||||
}
|
||||
}
|
||||
|
||||
removedItems.forEach((index, item) {
|
||||
_items.removeAt(index);
|
||||
_onRemoved.add(item);
|
||||
});
|
||||
|
||||
changesComputed = changesComputed || removedItems.isNotEmpty;
|
||||
}
|
||||
|
||||
if (checkForModified != false) {
|
||||
var modifiedItems = <int, dynamic>{};
|
||||
|
||||
for (var item in items) {
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
var localItem = _items[i];
|
||||
|
||||
if (compareId.equals(item, localItem)) {
|
||||
if (!compareItems.equals(item, localItem)) {
|
||||
modifiedItems[i] = item;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifiedItems.forEach((index, item) {
|
||||
_onModified.add(_items[index] = item);
|
||||
});
|
||||
|
||||
changesComputed = changesComputed || modifiedItems.isNotEmpty;
|
||||
}
|
||||
|
||||
if (!changesComputed) {
|
||||
_items
|
||||
..clear()
|
||||
..addAll(items);
|
||||
_onIndexed.add(items);
|
||||
}
|
||||
}
|
||||
}
|
14
packages/poll/poll.iml
Normal file
14
packages/poll/poll.iml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
14
packages/poll/pubspec.yaml
Normal file
14
packages/poll/pubspec.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: angel_poll
|
||||
version: 1.0.0
|
||||
description: package:angel_client support for "realtime" interactions with Angel via long polling.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
homepage: https://github.com/angel-dart/poll
|
||||
dependencies:
|
||||
angel_client: ^1.0.0
|
||||
async: ">=1.10.0 <3.0.0"
|
||||
collection: ^1.0.0
|
||||
dev_dependencies:
|
||||
angel_test: ^1.1.0
|
||||
test: ^0.12.0
|
105
packages/poll/test/all_test.dart
Normal file
105
packages/poll/test/all_test.dart
Normal file
|
@ -0,0 +1,105 @@
|
|||
import 'package:angel_framework/angel_framework.dart' as srv;
|
||||
import 'package:angel_poll/angel_poll.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
srv.Service store;
|
||||
TestClient client;
|
||||
PollingService pollingService;
|
||||
|
||||
setUp(() async {
|
||||
var app = new srv.Angel();
|
||||
app.logger = new Logger.detached('angel_poll')
|
||||
..onRecord.listen((rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) {
|
||||
print(rec.error);
|
||||
print(rec.stackTrace);
|
||||
}
|
||||
});
|
||||
|
||||
store = app.use(
|
||||
'/api/todos',
|
||||
new srv.MapService(
|
||||
autoIdAndDateFields: false,
|
||||
),
|
||||
);
|
||||
|
||||
client = await connectTo(app);
|
||||
|
||||
pollingService = new PollingService(
|
||||
client.service('api/todos'),
|
||||
const Duration(milliseconds: 100),
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
group('events', () {
|
||||
var created;
|
||||
StreamQueue onCreated, onModified, onRemoved;
|
||||
|
||||
setUp(() async {
|
||||
onCreated = new StreamQueue(pollingService.onCreated);
|
||||
onModified = new StreamQueue(pollingService.onModified);
|
||||
onRemoved = new StreamQueue(pollingService.onRemoved);
|
||||
|
||||
created = await store.create({
|
||||
'id': '0',
|
||||
'text': 'Clean your room',
|
||||
'completed': false,
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
onCreated.cancel();
|
||||
onModified.cancel();
|
||||
onRemoved.cancel();
|
||||
});
|
||||
|
||||
test('fires indexed', () async {
|
||||
var indexed = await pollingService.index();
|
||||
print(indexed);
|
||||
expect(await pollingService.onIndexed.first, indexed);
|
||||
});
|
||||
|
||||
test('fires created', () async {
|
||||
var result = await onCreated.next;
|
||||
print(result);
|
||||
expect(created, result);
|
||||
});
|
||||
|
||||
test('fires modified', () async {
|
||||
await pollingService.index();
|
||||
await store.modify('0', {
|
||||
'text': 'go to school',
|
||||
});
|
||||
|
||||
var result = await onModified.next;
|
||||
print(result);
|
||||
expect(result, new Map.from(created)..['text'] = 'go to school');
|
||||
});
|
||||
|
||||
test('manual modify', () async {
|
||||
await pollingService.index();
|
||||
await pollingService.modify('0', {
|
||||
'text': 'eat',
|
||||
});
|
||||
|
||||
var result = await onModified.next;
|
||||
print(result);
|
||||
expect(result, new Map.from(created)..['text'] = 'eat');
|
||||
});
|
||||
|
||||
test('fires removed', () async {
|
||||
await pollingService.index();
|
||||
var removed = await store.remove('0');
|
||||
var result = await onRemoved.next;
|
||||
print(result);
|
||||
expect(result, removed);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue