unfinished
This commit is contained in:
parent
27b0ab53ac
commit
d6d56f1f68
17 changed files with 361 additions and 0 deletions
64
.gitignore
vendored
Normal file
64
.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
.idea/misc.xml
Normal file
6
.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
.idea/modules.xml
Normal file
8
.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$/eventsource.iml" filepath="$PROJECT_DIR$/eventsource.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
7
.idea/runConfigurations/build_dart.xml
Normal file
7
.idea/runConfigurations/build_dart.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="build.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/tool/build.dart" />
|
||||||
|
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
7
.idea/runConfigurations/main_dart.xml
Normal file
7
.idea/runConfigurations/main_dart.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/example/main.dart" />
|
||||||
|
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
7
.idea/runConfigurations/watch_dart.xml
Normal file
7
.idea/runConfigurations/watch_dart.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="watch.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/tool/watch.dart" />
|
||||||
|
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
6
.idea/vcs.xml
Normal file
6
.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>
|
2
analysis_options.yaml
Normal file
2
analysis_options.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode: true
|
14
eventsource.iml
Normal file
14
eventsource.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>
|
51
example/main.dart
Normal file
51
example/main.dart
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_eventsource/server.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:eventsource/eventsource.dart';
|
||||||
|
import 'package:eventsource/publisher.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'pretty_logging.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
var app = new Angel();
|
||||||
|
|
||||||
|
app.use('/api/todos', new MapService());
|
||||||
|
|
||||||
|
var publisher = new AngelEventSourcePublisher(new EventSourcePublisher());
|
||||||
|
await app.configure(publisher.configureServer);
|
||||||
|
|
||||||
|
app.get('/sse', publisher.handleRequest);
|
||||||
|
|
||||||
|
app.logger = new Logger('angel_eventsource')..onRecord.listen(prettyLog);
|
||||||
|
|
||||||
|
var server = await app.startServer('127.0.0.1', 3000);
|
||||||
|
var url = Uri.parse('http://${server.address.address}:${server.port}');
|
||||||
|
print('Listening at $url');
|
||||||
|
|
||||||
|
/*
|
||||||
|
var sock = await Socket.connect(server.address, server.port);
|
||||||
|
sock
|
||||||
|
..writeln('GET /sse HTTP/1.1')
|
||||||
|
..writeln('Accept: text/event-stream')
|
||||||
|
..writeln('Host: 127.0.0.1')
|
||||||
|
..writeln()
|
||||||
|
..flush();
|
||||||
|
sock.transform(UTF8.decoder).transform(const LineSplitter()).listen(print);
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
var client = new HttpClient();
|
||||||
|
var rq = await client.openUrl('GET', url);
|
||||||
|
var rs = await rq.close();
|
||||||
|
rs.transform(UTF8.decoder).transform(const LineSplitter()).listen(print);
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
var eventSource = await EventSource.connect(url);
|
||||||
|
|
||||||
|
await for (var event in eventSource) {
|
||||||
|
print(event.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
example/pretty_logging.dart
Normal file
32
example/pretty_logging.dart
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:console/console.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
/// Prints the contents of a [LogRecord] with pretty colors.
|
||||||
|
prettyLog(LogRecord record) async {
|
||||||
|
var pen = new TextPen();
|
||||||
|
chooseLogColor(pen.reset(), record.level);
|
||||||
|
pen(record.toString());
|
||||||
|
|
||||||
|
if (record.error != null)
|
||||||
|
pen(record.error.toString());
|
||||||
|
if (record.stackTrace != null)
|
||||||
|
pen(record.stackTrace.toString());
|
||||||
|
|
||||||
|
pen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chooses a color based on the logger [level].
|
||||||
|
void chooseLogColor(TextPen pen, Level level) {
|
||||||
|
if (level == Level.SHOUT)
|
||||||
|
pen.darkRed();
|
||||||
|
else if (level == Level.SEVERE)
|
||||||
|
pen.red();
|
||||||
|
else if (level == Level.WARNING)
|
||||||
|
pen.yellow();
|
||||||
|
else if (level == Level.INFO)
|
||||||
|
pen.magenta();
|
||||||
|
else if (level == Level.FINER)
|
||||||
|
pen.blue();
|
||||||
|
else if (level == Level.FINEST)
|
||||||
|
pen.darkBlue();
|
||||||
|
}
|
1
lib/angel_eventsource.dart
Normal file
1
lib/angel_eventsource.dart
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export 'package:angel_websocket/angel_websocket.dart';
|
119
lib/server.dart
Normal file
119
lib/server.dart
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/hooks.dart' as hooks;
|
||||||
|
import 'package:angel_websocket/server.dart';
|
||||||
|
import 'package:eventsource/eventsource.dart';
|
||||||
|
import 'package:eventsource/src/encoder.dart';
|
||||||
|
import 'package:eventsource/publisher.dart';
|
||||||
|
import 'package:json_god/json_god.dart' as god;
|
||||||
|
|
||||||
|
class AngelEventSourcePublisher {
|
||||||
|
final EventSourcePublisher eventSourcePublisher;
|
||||||
|
|
||||||
|
/// Used to notify other nodes of an event's firing. Good for scaled applications.
|
||||||
|
final WebSocketSynchronizer synchronizer;
|
||||||
|
|
||||||
|
/// Serializes a [WebSocketEvent] to JSON.
|
||||||
|
///
|
||||||
|
/// Defaults to [god.serialize].
|
||||||
|
final String Function(WebSocketEvent) serializer;
|
||||||
|
int _count = 0;
|
||||||
|
|
||||||
|
AngelEventSourcePublisher(this.eventSourcePublisher,
|
||||||
|
{this.synchronizer, this.serializer});
|
||||||
|
|
||||||
|
Future configureServer(Angel app) async {
|
||||||
|
await app.configure(hooks.hookAllServices((service) {
|
||||||
|
if (service is HookedService) {
|
||||||
|
var path =
|
||||||
|
app.services.keys.firstWhere((p) => app.services[p] == service);
|
||||||
|
|
||||||
|
service.after([
|
||||||
|
HookedServiceEvent.created,
|
||||||
|
HookedServiceEvent.modified,
|
||||||
|
HookedServiceEvent.updated,
|
||||||
|
HookedServiceEvent.removed,
|
||||||
|
], (e) {
|
||||||
|
var event = new WebSocketEvent(
|
||||||
|
eventName: '${path.toString()}::${e.eventName}',
|
||||||
|
data: e.result,
|
||||||
|
);
|
||||||
|
|
||||||
|
_filter(RequestContext req, ResponseContext res) {
|
||||||
|
if (e.service.configuration.containsKey('sse:filter'))
|
||||||
|
return e.service.configuration['sse:filter'](e, req, res);
|
||||||
|
else if (e.params != null && e.params.containsKey('sse:filter'))
|
||||||
|
return e.params['sse:filter'](e, req, res);
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canSend = _filter(e.request, e.response);
|
||||||
|
|
||||||
|
if (canSend) {
|
||||||
|
batchEvent(event, e.request.properties['channel'] ?? '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (synchronizer != null) {
|
||||||
|
var sub = synchronizer.stream.listen((e) => batchEvent(e, ''));
|
||||||
|
app.shutdownHooks.add((_) async {
|
||||||
|
sub.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future batchEvent(WebSocketEvent event, String channel) async {
|
||||||
|
eventSourcePublisher.add(
|
||||||
|
new Event(
|
||||||
|
id: (_count++).toString(),
|
||||||
|
data: (serializer ?? god.serialize)(event),
|
||||||
|
),
|
||||||
|
channels: [channel],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
||||||
|
if (!req.accepts('text/event-stream', strict: false))
|
||||||
|
throw new AngelHttpException.badRequest();
|
||||||
|
|
||||||
|
res
|
||||||
|
..headers.addAll({
|
||||||
|
'cache-control': 'no-cache, no-store, must-revalidate',
|
||||||
|
'content-type': 'text/event-stream',
|
||||||
|
'connection': 'keep-alive',
|
||||||
|
})
|
||||||
|
..willCloseItself = true
|
||||||
|
..end();
|
||||||
|
|
||||||
|
var acceptsGzip =
|
||||||
|
(req.headers['accept-encoding']?.contains('gzip') == true);
|
||||||
|
|
||||||
|
if (acceptsGzip) res.io.headers.set('content-encoding', 'gzip');
|
||||||
|
res.headers.forEach(res.io.headers.set);
|
||||||
|
|
||||||
|
var sock = res.io ?? await res.io.detachSocket();
|
||||||
|
sock.flush();
|
||||||
|
|
||||||
|
var eventSink = new EventSourceEncoder(compressed: acceptsGzip)
|
||||||
|
.startChunkedConversion(sock);
|
||||||
|
|
||||||
|
eventSourcePublisher.newSubscription(
|
||||||
|
onEvent: (e) {
|
||||||
|
try {
|
||||||
|
eventSink.add(e);
|
||||||
|
sock.flush();
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore disconnect
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClose: eventSink.close,
|
||||||
|
channel: req.properties['channel'] ?? '',
|
||||||
|
lastEventId: req.headers.value('last-event-id'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
14
pubspec.yaml
Normal file
14
pubspec.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: "angel_eventsource"
|
||||||
|
dependencies:
|
||||||
|
angel_client: "^1.0.0"
|
||||||
|
angel_framework: "^1.0.0-dev"
|
||||||
|
angel_model: "^1.0.0"
|
||||||
|
angel_serialize: "^1.0.0-alpha"
|
||||||
|
angel_websocket: "^1.0.0"
|
||||||
|
eventsource: "^0.1.0"
|
||||||
|
json_god: ^2.0.0-beta
|
||||||
|
dev_dependencies:
|
||||||
|
angel_serialize_generator: "^1.0.0-alpha"
|
||||||
|
build_runner: "^0.5.0"
|
||||||
|
console: "^2.2.4"
|
||||||
|
test: "^0.12.0"
|
4
tool/build.dart
Normal file
4
tool/build.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import 'package:build_runner/build_runner.dart';
|
||||||
|
import 'build_actions.dart';
|
||||||
|
|
||||||
|
main() => build(buildActions, deleteFilesByDefault: true);
|
15
tool/build_actions.dart
Normal file
15
tool/build_actions.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:angel_serialize_generator/angel_serialize_generator.dart';
|
||||||
|
import 'package:build_runner/build_runner.dart';
|
||||||
|
import 'package:source_gen/source_gen.dart';
|
||||||
|
|
||||||
|
final List<BuildAction> buildActions = [
|
||||||
|
new BuildAction(
|
||||||
|
new PartBuilder([
|
||||||
|
const JsonModelGenerator(autoIdAndDateFields: false),
|
||||||
|
]),
|
||||||
|
'angel_eventsource',
|
||||||
|
inputs: const [
|
||||||
|
'lib/angel_eventsource.dart',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
4
tool/watch.dart
Normal file
4
tool/watch.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import 'package:build_runner/build_runner.dart';
|
||||||
|
import 'build_actions.dart';
|
||||||
|
|
||||||
|
main() => watch(buildActions, deleteFilesByDefault: true);
|
Loading…
Reference in a new issue