Add 'packages/websocket/' from commit '229b5e72058a644292d0f258bd0f7f06613b7b24'
git-subtree-dir: packages/websocket git-subtree-mainline:e887b1d21f
git-subtree-split:229b5e7205
This commit is contained in:
commit
c97363d290
29 changed files with 2260 additions and 0 deletions
76
packages/websocket/.gitignore
vendored
Normal file
76
packages/websocket/.gitignore
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.packages
|
||||
.project
|
||||
.pub/
|
||||
build/
|
||||
**/packages/
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
.idea
|
||||
|
||||
log.txt
|
||||
### 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
|
||||
|
||||
# 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
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
.dart_tool
|
4
packages/websocket/.travis.yml
Normal file
4
packages/websocket/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
dart:
|
||||
- dev
|
||||
- stable
|
57
packages/websocket/CHANGELOG.md
Normal file
57
packages/websocket/CHANGELOG.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# 2.0.3
|
||||
* Remove `WebSocketController.plugin`.
|
||||
* Remove any unawaited futures.
|
||||
|
||||
# 2.0.2
|
||||
* Update `stream_channel` to `2.0.0`.
|
||||
* Use `angel_framework^@2.0.0-rc.0`.
|
||||
|
||||
# 2.0.1
|
||||
* Add `reconnectOnClose` and `reconnectinterval` parameters in top-level `WebSockets` constructors.
|
||||
* Close `WebSocketExtraneousEventHandler`.
|
||||
* Add onAuthenticated to server-side.
|
||||
|
||||
# 2.0.0
|
||||
* Update to work with `client@2.0.0`.
|
||||
|
||||
# 2.0.0-alpha.8
|
||||
* Support for WebSockets over HTTP/2 (though in practice this doesn't often happen, if ever).
|
||||
|
||||
# 2.0.0-alpha.7
|
||||
* Replace `WebSocketSynchronizer` with `StreamChannel<WebSocketEvent>`.
|
||||
|
||||
# 2.0.0-alpha.6
|
||||
* Explicit import of `import 'package:http/io_client.dart' as http;`
|
||||
|
||||
# 2.0.0-alpha.5
|
||||
* Update `http` dependency.
|
||||
|
||||
# 2.0.0-alpha.4
|
||||
* Remove `package:json_god`.
|
||||
* Make `WebSocketContext` take any `StreamChannel`.
|
||||
* Strong typing updates.
|
||||
|
||||
# 2.0.0-alpha.3
|
||||
* Directly import Angel HTTP.
|
||||
|
||||
# 2.0.0-alpha.2
|
||||
* Updated for the next version of `angel_client`.
|
||||
|
||||
# 2.0.0-alpha.1
|
||||
* Refactorings for updated Angel 2 versions.
|
||||
* Remove `package:dart2_constant`.
|
||||
|
||||
# 2.0.0-alpha
|
||||
* Depend on Dart 2 and Angel 2.
|
||||
|
||||
# 1.1.2
|
||||
* Dart 2 updates.
|
||||
* Added `handleClient`, which is nice for external implementations
|
||||
that plug into `AngelWebSocket`.
|
||||
|
||||
# 1.1.1
|
||||
* Deprecated `unwrap`.
|
||||
* Service streams now pump out `e.data`, rather than the actual event.
|
||||
|
||||
# 1.1.0+1
|
||||
* Added `unwrap`.
|
21
packages/websocket/LICENSE
Normal file
21
packages/websocket/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 angel-dart
|
||||
|
||||
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.
|
163
packages/websocket/README.md
Normal file
163
packages/websocket/README.md
Normal file
|
@ -0,0 +1,163 @@
|
|||
# angel_websocket
|
||||
[![Pub](https://img.shields.io/pub/v/angel_websocket.svg)](https://pub.dartlang.org/packages/angel_websocket)
|
||||
[![build status](https://travis-ci.org/angel-dart/websocket.svg)](https://travis-ci.org/angel-dart/websocket)
|
||||
|
||||
WebSocket plugin for Angel.
|
||||
|
||||
This plugin broadcasts events from hooked services via WebSockets.
|
||||
|
||||
In addition, it adds itself to the app's IoC container as `AngelWebSocket`, so that it can be used
|
||||
in controllers as well.
|
||||
|
||||
WebSocket contexts are add to `req.properties` as `'socket'`.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
**Server-side**
|
||||
|
||||
```dart
|
||||
import "package:angel_framework/angel_framework.dart";
|
||||
import "package:angel_websocket/server.dart";
|
||||
|
||||
main() async {
|
||||
var app = new Angel();
|
||||
|
||||
var ws = new AngelWebSocket();
|
||||
|
||||
// This is a plug-in. It hooks all your services,
|
||||
// to automatically broadcast events.
|
||||
await app.configure(ws.configureServer);
|
||||
|
||||
// Listen for requests at `/ws`.
|
||||
app.all('/ws', ws.handleRequest);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Filtering events is easy with hooked services. Just return a `bool`, whether
|
||||
synchronously or asynchronously.
|
||||
|
||||
```dart
|
||||
myService.properties['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
myService.index({
|
||||
'ws:filter': (e, socket) => ...;
|
||||
});
|
||||
```
|
||||
|
||||
**Adding Handlers within a Controller**
|
||||
|
||||
`WebSocketController` extends a normal `Controller`, but also listens to WebSockets.
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
import "package:angel_framework/angel_framework.dart";
|
||||
import "package:angel_websocket/server.dart";
|
||||
|
||||
@Expose("/")
|
||||
class MyController extends WebSocketController {
|
||||
// A reference to the WebSocket plug-in is required.
|
||||
MyController(AngelWebSocket ws):super(ws);
|
||||
|
||||
@override
|
||||
void onConnect(WebSocketContext socket) {
|
||||
// On connect...
|
||||
}
|
||||
|
||||
// Dependency injection works, too..
|
||||
@ExposeWs("read_message")
|
||||
void sendMessage(WebSocketContext socket, WebSocketAction action, Db db) async {
|
||||
socket.send(
|
||||
"found_message",
|
||||
db.collection("messages").findOne(where.id(action.data['message_id'])));
|
||||
}
|
||||
|
||||
// Event filtering
|
||||
@ExposeWs("foo")
|
||||
void foo() {
|
||||
broadcast(new WebSocketEvent(...), filter: (socket) async => ...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client Use**
|
||||
|
||||
This repo also provides two client libraries `browser` and `io` that extend the base
|
||||
`angel_client` interface, and allow you to use a very similar API on the client to that of
|
||||
the server.
|
||||
|
||||
The provided clients also automatically try to reconnect their WebSockets when disconnected,
|
||||
which means you can restart your development server without having to reload browser windows.
|
||||
|
||||
They also provide streams of data that pump out filtered data as it comes in from the server.
|
||||
|
||||
Clients can even perform authentication over WebSockets.
|
||||
|
||||
**In the Browser**
|
||||
|
||||
```dart
|
||||
import "package:angel_websocket/browser.dart";
|
||||
|
||||
main() async {
|
||||
Angel app = new WebSockets("/ws");
|
||||
await app.connect();
|
||||
|
||||
var Cars = app.service("api/cars");
|
||||
|
||||
Cars.onCreated.listen((car) => print("New car: $car"));
|
||||
|
||||
// Happens asynchronously
|
||||
Cars.create({"brand": "Toyota"});
|
||||
|
||||
// Authenticate a WebSocket, if you were not already authenticated...
|
||||
app.authenticateViaJwt('<some-jwt>');
|
||||
|
||||
// Listen for arbitrary events
|
||||
app.on['custom_event'].listen((event) {
|
||||
// For example, this might be sent by a
|
||||
// WebSocketController.
|
||||
print('Hi!');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**CLI Client**
|
||||
|
||||
```dart
|
||||
import "package:angel_framework/common.dart";
|
||||
import "package:angel_websocket/io.dart";
|
||||
|
||||
// You can include these in a shared file and access on both client and server
|
||||
class Car extends Model {
|
||||
int year;
|
||||
String brand, make;
|
||||
|
||||
Car({this.year, this.brand, this.make});
|
||||
|
||||
@override String toString() => "$year $brand $make";
|
||||
}
|
||||
|
||||
main() async {
|
||||
Angel app = new WebSockets("/ws");
|
||||
|
||||
// Wait for WebSocket connection...
|
||||
await app.connect();
|
||||
|
||||
var Cars = app.service("api/cars", type: Car);
|
||||
|
||||
Cars.onCreated.listen((Car car) {
|
||||
// Automatically deserialized into a car :)
|
||||
//
|
||||
// I just bought a new 2016 Toyota Camry!
|
||||
print("I just bought a new $car!");
|
||||
});
|
||||
|
||||
// Happens asynchronously
|
||||
Cars.create({"year": 2016, "brand": "Toyota", "make": "Camry"});
|
||||
|
||||
// Authenticate a WebSocket, if you were not already authenticated...
|
||||
app.authenticateViaJwt('<some-jwt>');
|
||||
}
|
4
packages/websocket/analysis_options.yaml
Normal file
4
packages/websocket/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
29
packages/websocket/dev.key
Normal file
29
packages/websocket/dev.key
Normal file
|
@ -0,0 +1,29 @@
|
|||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
|
||||
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
|
||||
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
|
||||
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
|
||||
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
|
||||
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
|
||||
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
|
||||
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
|
||||
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
|
||||
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
|
||||
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
|
||||
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
|
||||
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
|
||||
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
|
||||
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
|
||||
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
|
||||
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
|
||||
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
|
||||
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
|
||||
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
|
||||
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
|
||||
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
|
||||
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
|
||||
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
|
||||
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
|
||||
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
|
||||
AUukhVtTNn4=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
57
packages/websocket/dev.pem
Normal file
57
packages/websocket/dev.pem
Normal file
|
@ -0,0 +1,57 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
|
||||
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
|
||||
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
|
||||
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
|
||||
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
|
||||
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
|
||||
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
|
||||
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
|
||||
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
|
||||
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
|
||||
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
|
||||
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
|
||||
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
|
||||
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
|
||||
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
|
||||
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
|
||||
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
|
||||
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
|
||||
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
|
||||
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
|
||||
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
|
||||
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
|
||||
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
|
||||
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
|
||||
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
|
||||
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
|
||||
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
|
||||
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
|
||||
FKvRDxsW
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
|
||||
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
|
||||
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
|
||||
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
|
||||
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
|
||||
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
|
||||
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
|
||||
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
|
||||
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
|
||||
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
|
||||
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
|
||||
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
|
||||
r6AL284qtw==
|
||||
-----END CERTIFICATE-----
|
30
packages/websocket/example/index.html
Normal file
30
packages/websocket/example/index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Angel WS</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
var url = location.protocol === "https:" ? "wss://" : "ws://";
|
||||
url += location.hostname;
|
||||
if (location.port) url += ":" + location.port;
|
||||
url += "/ws";
|
||||
|
||||
var ws = new WebSocket(url);
|
||||
window.ws = ws;
|
||||
ws.onmessage = function(msg) {
|
||||
console.info(JSON.parse(JSON.parse(msg.data).data));
|
||||
};
|
||||
|
||||
window.sendWs = function(msg) {
|
||||
var data = { type: "ping", data: msg };
|
||||
ws.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
console.info('Connected! Type sendWs("Hey!") to play around.');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
56
packages/websocket/example/main.dart
Normal file
56
packages/websocket/example/main.dart
Normal file
|
@ -0,0 +1,56 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel_framework/http2.dart';
|
||||
import 'package:angel_websocket/server.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
main(List<String> args) async {
|
||||
var app = new Angel();
|
||||
var http = new AngelHttp(app);
|
||||
var ws = new AngelWebSocket(app, sendErrors: !app.environment.isProduction);
|
||||
var fs = const LocalFileSystem();
|
||||
app.logger = new Logger('angel_websocket');
|
||||
|
||||
// This is a plug-in. It hooks all your services,
|
||||
// to automatically broadcast events.
|
||||
await app.configure(ws.configureServer);
|
||||
|
||||
app.get('/', (req, res) => res.streamFile(fs.file('example/index.html')));
|
||||
|
||||
// Listen for requests at `/ws`.
|
||||
app.get('/ws', ws.handleRequest);
|
||||
|
||||
app.fallback((req, res) => throw AngelHttpException.notFound());
|
||||
|
||||
ws.onConnection.listen((socket) {
|
||||
socket.onData.listen((x) {
|
||||
socket.send('pong', x);
|
||||
});
|
||||
});
|
||||
|
||||
if (args.contains('http2')) {
|
||||
var ctx = new SecurityContext()
|
||||
..useCertificateChain('dev.pem')
|
||||
..usePrivateKey('dev.key', password: 'dartdart');
|
||||
|
||||
try {
|
||||
ctx.setAlpnProtocols(['h2'], true);
|
||||
} catch (e, st) {
|
||||
app.logger.severe(
|
||||
'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.',
|
||||
e,
|
||||
st,
|
||||
);
|
||||
}
|
||||
|
||||
var http2 = new AngelHttp2(app, ctx);
|
||||
http2.onHttp1.listen(http.handleRequest);
|
||||
await http2.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http2.uri}');
|
||||
} else {
|
||||
await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http.uri}');
|
||||
}
|
||||
}
|
46
packages/websocket/lib/angel_websocket.dart
Normal file
46
packages/websocket/lib/angel_websocket.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
/// WebSocket plugin for Angel.
|
||||
library angel_websocket;
|
||||
|
||||
/// A notification from the server that something has occurred.
|
||||
class WebSocketEvent<Data> {
|
||||
String eventName;
|
||||
Data data;
|
||||
|
||||
WebSocketEvent({String this.eventName, this.data});
|
||||
|
||||
factory WebSocketEvent.fromJson(Map data) => new WebSocketEvent(
|
||||
eventName: data['eventName'].toString(), data: data['data'] as Data);
|
||||
|
||||
WebSocketEvent<T> cast<T>() {
|
||||
if (T == Data) {
|
||||
return this as WebSocketEvent<T>;
|
||||
} else {
|
||||
return new WebSocketEvent<T>(eventName: eventName, data: data as T);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'eventName': eventName, 'data': data};
|
||||
}
|
||||
}
|
||||
|
||||
/// A command sent to the server, usually corresponding to a service method.
|
||||
class WebSocketAction {
|
||||
String id;
|
||||
String eventName;
|
||||
var data;
|
||||
Map<String, dynamic> params;
|
||||
|
||||
WebSocketAction(
|
||||
{String this.id, String this.eventName, this.data, this.params});
|
||||
|
||||
factory WebSocketAction.fromJson(Map data) => new WebSocketAction(
|
||||
id: data['id'].toString(),
|
||||
eventName: data['eventName'].toString(),
|
||||
data: data['data'],
|
||||
params: data['params'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'eventName': eventName, 'data': data, 'params': params};
|
||||
}
|
||||
}
|
446
packages/websocket/lib/base_websocket_client.dart
Normal file
446
packages/websocket/lib/base_websocket_client.dart
Normal file
|
@ -0,0 +1,446 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'package:angel_client/angel_client.dart';
|
||||
import 'package:angel_client/base_angel_client.dart';
|
||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:http/src/base_client.dart' as http;
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/status.dart' as status;
|
||||
import 'angel_websocket.dart';
|
||||
import 'constants.dart';
|
||||
|
||||
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||
|
||||
/// An [Angel] client that operates across WebSockets.
|
||||
abstract class BaseWebSocketClient extends BaseAngelClient {
|
||||
Duration _reconnectInterval;
|
||||
WebSocketChannel _socket;
|
||||
final Queue<WebSocketAction> _queue = new Queue<WebSocketAction>();
|
||||
|
||||
final StreamController _onData = new StreamController();
|
||||
final StreamController<WebSocketEvent> _onAllEvents =
|
||||
new StreamController<WebSocketEvent>();
|
||||
final StreamController<AngelAuthResult> _onAuthenticated =
|
||||
new StreamController<AngelAuthResult>();
|
||||
final StreamController<AngelHttpException> _onError =
|
||||
new StreamController<AngelHttpException>();
|
||||
final StreamController<Map<String, WebSocketEvent>> _onServiceEvent =
|
||||
new StreamController<Map<String, WebSocketEvent>>.broadcast();
|
||||
final StreamController<WebSocketChannelException>
|
||||
_onWebSocketChannelException =
|
||||
new StreamController<WebSocketChannelException>();
|
||||
|
||||
/// Use this to handle events that are not standard.
|
||||
final WebSocketExtraneousEventHandler on =
|
||||
new WebSocketExtraneousEventHandler();
|
||||
|
||||
/// Fired on all events.
|
||||
Stream<WebSocketEvent> get onAllEvents => _onAllEvents.stream;
|
||||
|
||||
/// Fired whenever a WebSocket is successfully authenticated.
|
||||
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
|
||||
|
||||
/// A broadcast stream of data coming from the [socket].
|
||||
///
|
||||
/// Mostly just for internal use.
|
||||
Stream get onData => _onData.stream;
|
||||
|
||||
/// Fired on errors.
|
||||
Stream<AngelHttpException> get onError => _onError.stream;
|
||||
|
||||
/// Fired whenever an event is fired by a service.
|
||||
Stream<Map<String, WebSocketEvent>> get onServiceEvent =>
|
||||
_onServiceEvent.stream;
|
||||
|
||||
/// Fired on [WebSocketChannelException]s.
|
||||
Stream<WebSocketChannelException> get onWebSocketChannelException =>
|
||||
_onWebSocketChannelException.stream;
|
||||
|
||||
/// The [WebSocketChannel] underneath this instance.
|
||||
WebSocketChannel get socket => _socket;
|
||||
|
||||
/// If `true` (default), then the client will automatically try to reconnect to the server
|
||||
/// if the socket closes.
|
||||
final bool reconnectOnClose;
|
||||
|
||||
/// The amount of time to wait between reconnect attempts. Default: 10 seconds.
|
||||
Duration get reconnectInterval => _reconnectInterval;
|
||||
|
||||
Uri _wsUri;
|
||||
|
||||
/// The [Uri] to which a websocket should point.
|
||||
Uri get websocketUri => _wsUri ??= _toWsUri(baseUrl);
|
||||
|
||||
static Uri _toWsUri(Uri u) {
|
||||
if (u.hasScheme) {
|
||||
if (u.scheme == 'http') {
|
||||
return u.replace(scheme: 'ws');
|
||||
} else if (u.scheme == 'https') {
|
||||
return u.replace(scheme: 'wss');
|
||||
} else {
|
||||
return u;
|
||||
}
|
||||
} else {
|
||||
return _toWsUri(u.replace(scheme: Uri.base.scheme));
|
||||
}
|
||||
}
|
||||
|
||||
BaseWebSocketClient(http.BaseClient client, baseUrl,
|
||||
{this.reconnectOnClose = true, Duration reconnectInterval})
|
||||
: super(client, baseUrl) {
|
||||
_reconnectInterval = reconnectInterval ?? new Duration(seconds: 10);
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() async {
|
||||
on._close();
|
||||
scheduleMicrotask(() async {
|
||||
await _socket.sink.close(status.goingAway);
|
||||
await _onData.close();
|
||||
await _onAllEvents.close();
|
||||
await _onAuthenticated.close();
|
||||
await _onError.close();
|
||||
await _onServiceEvent.close();
|
||||
await _onWebSocketChannelException.close();
|
||||
});
|
||||
}
|
||||
|
||||
/// Connects the WebSocket. [timeout] is optional.
|
||||
Future<WebSocketChannel> connect({Duration timeout}) async {
|
||||
if (timeout != null) {
|
||||
var c = new Completer<WebSocketChannel>();
|
||||
Timer timer;
|
||||
|
||||
timer = new Timer(timeout, () {
|
||||
if (!c.isCompleted) {
|
||||
if (timer.isActive) timer.cancel();
|
||||
c.completeError(new TimeoutException(
|
||||
'WebSocket connection exceeded timeout of ${timeout.inMilliseconds} ms',
|
||||
timeout));
|
||||
}
|
||||
});
|
||||
|
||||
scheduleMicrotask(() {
|
||||
return getConnectedWebSocket().then((socket) {
|
||||
if (!c.isCompleted) {
|
||||
if (timer.isActive) timer.cancel();
|
||||
|
||||
while (_queue.isNotEmpty) {
|
||||
var action = _queue.removeFirst();
|
||||
socket.sink.add(serialize(action));
|
||||
}
|
||||
|
||||
c.complete(socket);
|
||||
}
|
||||
}).catchError((e, StackTrace st) {
|
||||
if (!c.isCompleted) {
|
||||
if (timer.isActive) timer.cancel();
|
||||
c.completeError(e, st);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return await c.future.then((socket) {
|
||||
_socket = socket;
|
||||
listen();
|
||||
});
|
||||
} else {
|
||||
_socket = await getConnectedWebSocket();
|
||||
listen();
|
||||
return _socket;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new [WebSocketChannel], ready to be listened on.
|
||||
///
|
||||
/// This should be overriden by child classes, **NOT** [connect].
|
||||
Future<WebSocketChannel> getConnectedWebSocket();
|
||||
|
||||
@override
|
||||
WebSocketsService<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer<Data> deserializer}) {
|
||||
String uri = path.toString().replaceAll(_straySlashes, '');
|
||||
return new WebSocketsService<Id, Data>(socket, this, uri,
|
||||
deserializer: deserializer);
|
||||
}
|
||||
|
||||
/// Starts listening for data.
|
||||
void listen() {
|
||||
_socket?.stream?.listen(
|
||||
(data) {
|
||||
_onData.add(data);
|
||||
|
||||
if (data is WebSocketChannelException) {
|
||||
_onWebSocketChannelException.add(data);
|
||||
} else if (data is String) {
|
||||
var jsons = json.decode(data);
|
||||
|
||||
if (jsons is Map) {
|
||||
var event = new WebSocketEvent.fromJson(jsons);
|
||||
|
||||
if (event.eventName?.isNotEmpty == true) {
|
||||
_onAllEvents.add(event);
|
||||
on._getStream(event.eventName).add(event);
|
||||
}
|
||||
|
||||
if (event.eventName == errorEvent) {
|
||||
var error =
|
||||
new AngelHttpException.fromMap((event.data ?? {}) as Map);
|
||||
_onError.add(error);
|
||||
} else if (event.eventName == authenticatedEvent) {
|
||||
var authResult = new AngelAuthResult.fromMap(event.data as Map);
|
||||
_onAuthenticated.add(authResult);
|
||||
} else if (event.eventName?.isNotEmpty == true) {
|
||||
var split = event.eventName
|
||||
.split("::")
|
||||
.where((str) => str.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
if (split.length >= 2) {
|
||||
var serviceName = split[0], eventName = split[1];
|
||||
_onServiceEvent
|
||||
.add({serviceName: event..eventName = eventName});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
onDone: () {
|
||||
_socket = null;
|
||||
if (reconnectOnClose == true) {
|
||||
new Timer.periodic(reconnectInterval, (Timer timer) async {
|
||||
var result;
|
||||
|
||||
try {
|
||||
result = await connect(timeout: reconnectInterval);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
if (result != null) timer.cancel();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Serializes data to JSON.
|
||||
serialize(x) => json.encode(x);
|
||||
|
||||
/// Sends the given [action] on the [socket].
|
||||
void sendAction(WebSocketAction action) {
|
||||
if (_socket == null)
|
||||
_queue.addLast(action);
|
||||
else
|
||||
socket.sink.add(serialize(action));
|
||||
}
|
||||
|
||||
/// Attempts to authenticate a WebSocket, using a valid JWT.
|
||||
void authenticateViaJwt(String jwt) {
|
||||
sendAction(new WebSocketAction(
|
||||
eventName: authenticateAction,
|
||||
params: {
|
||||
'query': {'jwt': jwt}
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Service] that asynchronously interacts with the server.
|
||||
class WebSocketsService<Id, Data> extends Service<Id, Data> {
|
||||
/// The [BaseWebSocketClient] that spawned this service.
|
||||
@override
|
||||
final BaseWebSocketClient app;
|
||||
|
||||
/// Used to deserialize JSON into typed data.
|
||||
final AngelDeserializer<Data> deserializer;
|
||||
|
||||
/// The [WebSocketChannel] to listen to, and send data across.
|
||||
final WebSocketChannel socket;
|
||||
|
||||
/// The service path to listen to.
|
||||
final String path;
|
||||
|
||||
final StreamController<WebSocketEvent> _onAllEvents =
|
||||
new StreamController<WebSocketEvent>();
|
||||
final StreamController<List<Data>> _onIndexed = new StreamController();
|
||||
final StreamController<Data> _onRead = new StreamController<Data>();
|
||||
final StreamController<Data> _onCreated = new StreamController<Data>();
|
||||
final StreamController<Data> _onModified = new StreamController<Data>();
|
||||
final StreamController<Data> _onUpdated = new StreamController<Data>();
|
||||
final StreamController<Data> _onRemoved = new StreamController<Data>();
|
||||
|
||||
/// Fired on all events.
|
||||
Stream<WebSocketEvent> get onAllEvents => _onAllEvents.stream;
|
||||
|
||||
/// Fired on `index` events.
|
||||
Stream<List<Data>> get onIndexed => _onIndexed.stream;
|
||||
|
||||
/// Fired on `read` events.
|
||||
Stream<Data> get onRead => _onRead.stream;
|
||||
|
||||
/// Fired on `created` events.
|
||||
Stream<Data> get onCreated => _onCreated.stream;
|
||||
|
||||
/// Fired on `modified` events.
|
||||
Stream<Data> get onModified => _onModified.stream;
|
||||
|
||||
/// Fired on `updated` events.
|
||||
Stream<Data> get onUpdated => _onUpdated.stream;
|
||||
|
||||
/// Fired on `removed` events.
|
||||
Stream<Data> get onRemoved => _onRemoved.stream;
|
||||
|
||||
WebSocketsService(this.socket, this.app, this.path, {this.deserializer}) {
|
||||
listen();
|
||||
}
|
||||
|
||||
Future close() async {
|
||||
await _onAllEvents.close();
|
||||
await _onCreated.close();
|
||||
await _onIndexed.close();
|
||||
await _onModified.close();
|
||||
await _onRead.close();
|
||||
await _onRemoved.close();
|
||||
await _onUpdated.close();
|
||||
}
|
||||
|
||||
/// Serializes an [action] to be sent over a WebSocket.
|
||||
serialize(WebSocketAction action) => json.encode(action);
|
||||
|
||||
/// Deserializes data from a [WebSocketEvent].
|
||||
Data deserialize(x) {
|
||||
return deserializer != null ? deserializer(x) : x as Data;
|
||||
}
|
||||
|
||||
/// Deserializes the contents of an [event].
|
||||
WebSocketEvent<Data> transformEvent(WebSocketEvent event) {
|
||||
return new WebSocketEvent(
|
||||
eventName: event.eventName, data: deserialize(event.data));
|
||||
}
|
||||
|
||||
/// Starts listening for events.
|
||||
void listen() {
|
||||
app.onServiceEvent.listen((map) {
|
||||
if (map.containsKey(path)) {
|
||||
var event = map[path];
|
||||
|
||||
_onAllEvents.add(event);
|
||||
|
||||
if (event.eventName == indexedEvent) {
|
||||
var d = event.data;
|
||||
var transformed = new WebSocketEvent(
|
||||
eventName: event.eventName,
|
||||
data: d is Iterable ? d.map(deserialize).toList() : null);
|
||||
if (transformed.data != null) _onIndexed.add(transformed.data);
|
||||
return;
|
||||
}
|
||||
|
||||
var transformed = transformEvent(event).data;
|
||||
|
||||
switch (event.eventName) {
|
||||
case readEvent:
|
||||
_onRead.add(transformed);
|
||||
break;
|
||||
case createdEvent:
|
||||
_onCreated.add(transformed);
|
||||
break;
|
||||
case modifiedEvent:
|
||||
_onModified.add(transformed);
|
||||
break;
|
||||
case updatedEvent:
|
||||
_onUpdated.add(transformed);
|
||||
break;
|
||||
case removedEvent:
|
||||
_onRemoved.add(transformed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends the given [action] on the [socket].
|
||||
void send(WebSocketAction action) {
|
||||
app.sendAction(action);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Data>> index([Map<String, dynamic> params]) async {
|
||||
app.sendAction(new WebSocketAction(
|
||||
eventName: '$path::$indexAction', params: params ?? {}));
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> read(id, [Map<String, dynamic> params]) async {
|
||||
app.sendAction(new WebSocketAction(
|
||||
eventName: '$path::$readAction',
|
||||
id: id.toString(),
|
||||
params: params ?? {}));
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> create(data, [Map<String, dynamic> params]) async {
|
||||
app.sendAction(new WebSocketAction(
|
||||
eventName: '$path::$createAction', data: data, params: params ?? {}));
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> modify(id, data, [Map<String, dynamic> params]) async {
|
||||
app.sendAction(new WebSocketAction(
|
||||
eventName: '$path::$modifyAction',
|
||||
id: id.toString(),
|
||||
data: data,
|
||||
params: params ?? {}));
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> update(id, data, [Map<String, dynamic> params]) async {
|
||||
app.sendAction(new WebSocketAction(
|
||||
eventName: '$path::$updateAction',
|
||||
id: id.toString(),
|
||||
data: data,
|
||||
params: params ?? {}));
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> remove(id, [Map<String, dynamic> params]) async {
|
||||
app.sendAction(new WebSocketAction(
|
||||
eventName: '$path::$removeAction',
|
||||
id: id.toString(),
|
||||
params: params ?? {}));
|
||||
return null;
|
||||
}
|
||||
|
||||
/// No longer necessary.
|
||||
@deprecated
|
||||
Service unwrap() => this;
|
||||
}
|
||||
|
||||
/// Contains a dynamic Map of [WebSocketEvent] streams.
|
||||
class WebSocketExtraneousEventHandler {
|
||||
Map<String, StreamController<WebSocketEvent>> _events = {};
|
||||
|
||||
StreamController<WebSocketEvent> _getStream(String index) {
|
||||
if (_events[index] == null)
|
||||
_events[index] = new StreamController<WebSocketEvent>();
|
||||
|
||||
return _events[index];
|
||||
}
|
||||
|
||||
Stream<WebSocketEvent> operator [](String index) {
|
||||
if (_events[index] == null)
|
||||
_events[index] = new StreamController<WebSocketEvent>();
|
||||
|
||||
return _events[index].stream;
|
||||
}
|
||||
|
||||
void _close() {
|
||||
_events.values.forEach((s) => s.close());
|
||||
}
|
||||
}
|
110
packages/websocket/lib/browser.dart
Normal file
110
packages/websocket/lib/browser.dart
Normal file
|
@ -0,0 +1,110 @@
|
|||
/// Browser WebSocket client library for the Angel framework.
|
||||
library angel_websocket.browser;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'package:angel_client/angel_client.dart';
|
||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:http/browser_client.dart' as http;
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/html.dart';
|
||||
import 'base_websocket_client.dart';
|
||||
export 'angel_websocket.dart';
|
||||
|
||||
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||
|
||||
/// Queries an Angel server via WebSockets.
|
||||
class WebSockets extends BaseWebSocketClient {
|
||||
final List<BrowserWebSocketsService> _services = [];
|
||||
|
||||
WebSockets(baseUrl,
|
||||
{bool reconnectOnClose = true, Duration reconnectInterval})
|
||||
: super(new http.BrowserClient(), baseUrl,
|
||||
reconnectOnClose: reconnectOnClose,
|
||||
reconnectInterval: reconnectInterval);
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
for (var service in _services) {
|
||||
service.close();
|
||||
}
|
||||
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token', String errorMessage}) {
|
||||
var ctrl = new StreamController<String>();
|
||||
var wnd = window.open(url, 'angel_client_auth_popup');
|
||||
|
||||
Timer t;
|
||||
StreamSubscription<Event> sub;
|
||||
t = new Timer.periodic(new Duration(milliseconds: 500), (timer) {
|
||||
if (!ctrl.isClosed) {
|
||||
if (wnd.closed) {
|
||||
ctrl.addError(new AngelHttpException.notAuthenticated(
|
||||
message:
|
||||
errorMessage ?? 'Authentication via popup window failed.'));
|
||||
ctrl.close();
|
||||
timer.cancel();
|
||||
sub?.cancel();
|
||||
}
|
||||
} else
|
||||
timer.cancel();
|
||||
});
|
||||
|
||||
sub = window.on[eventName ?? 'token'].listen((e) {
|
||||
if (!ctrl.isClosed) {
|
||||
ctrl.add((e as CustomEvent).detail.toString());
|
||||
t.cancel();
|
||||
ctrl.close();
|
||||
sub.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
return ctrl.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WebSocketChannel> getConnectedWebSocket() {
|
||||
var url = websocketUri;
|
||||
|
||||
if (authToken?.isNotEmpty == true) {
|
||||
url = url.replace(
|
||||
queryParameters: new Map<String, String>.from(url.queryParameters)
|
||||
..['token'] = authToken);
|
||||
}
|
||||
|
||||
var socket = new WebSocket(url.toString());
|
||||
var completer = new Completer<WebSocketChannel>();
|
||||
|
||||
socket
|
||||
..onOpen.listen((_) {
|
||||
if (!completer.isCompleted)
|
||||
return completer.complete(new HtmlWebSocketChannel(socket));
|
||||
})
|
||||
..onError.listen((e) {
|
||||
if (!completer.isCompleted)
|
||||
return completer.completeError(e is ErrorEvent ? e.error : e);
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
BrowserWebSocketsService<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer<Data> deserializer}) {
|
||||
String uri = path.replaceAll(_straySlashes, '');
|
||||
return new BrowserWebSocketsService<Id, Data>(socket, this, uri,
|
||||
deserializer: deserializer);
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserWebSocketsService<Id, Data> extends WebSocketsService<Id, Data> {
|
||||
final Type type;
|
||||
|
||||
BrowserWebSocketsService(WebSocketChannel socket, WebSockets app, String uri,
|
||||
{this.type, AngelDeserializer<Data> deserializer})
|
||||
: super(socket, app, uri, deserializer: deserializer);
|
||||
}
|
87
packages/websocket/lib/constants.dart
Normal file
87
packages/websocket/lib/constants.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
const String authenticateAction = 'authenticate';
|
||||
const String indexAction = 'index';
|
||||
const String readAction = 'read';
|
||||
const String createAction = 'create';
|
||||
const String modifyAction = 'modify';
|
||||
const String updateAction = 'update';
|
||||
const String removeAction = 'remove';
|
||||
|
||||
@deprecated
|
||||
const String ACTION_AUTHENTICATE = authenticateAction;
|
||||
|
||||
@deprecated
|
||||
const String ACTION_INDEX = indexAction;
|
||||
|
||||
@deprecated
|
||||
const String ACTION_READ = readAction;
|
||||
|
||||
@deprecated
|
||||
const String ACTION_CREATE = createAction;
|
||||
|
||||
@deprecated
|
||||
const String ACTION_MODIFY = modifyAction;
|
||||
|
||||
@deprecated
|
||||
const String ACTION_UPDATE = updateAction;
|
||||
|
||||
@deprecated
|
||||
const String ACTION_REMOVE = removeAction;
|
||||
|
||||
const String authenticatedEvent = 'authenticated';
|
||||
const String errorEvent = 'error';
|
||||
const String indexedEvent = 'indexed';
|
||||
const String readEvent = 'read';
|
||||
const String createdEvent = 'created';
|
||||
const String modifiedEvent = 'modified';
|
||||
const String updatedEvent = 'updated';
|
||||
const String removedEvent = 'removed';
|
||||
|
||||
@deprecated
|
||||
const String EVENT_AUTHENTICATED = authenticatedEvent;
|
||||
|
||||
@deprecated
|
||||
const String EVENT_ERROR = errorEvent;
|
||||
|
||||
@deprecated
|
||||
const String EVENT_INDEXED = indexedEvent;
|
||||
|
||||
@deprecated
|
||||
const String EVENT_READ = readEvent;
|
||||
|
||||
@deprecated
|
||||
const String EVENT_CREATED = createdEvent;
|
||||
|
||||
@deprecated
|
||||
const String EVENT_MODIFIED = modifiedEvent;
|
||||
|
||||
@deprecated
|
||||
const String EVENT_UPDATED = updatedEvent;
|
||||
|
||||
@deprecated
|
||||
const String EVENT_REMOVED = removedEvent;
|
||||
|
||||
/// The standard Angel service actions.
|
||||
const List<String> actions = const <String>[
|
||||
indexAction,
|
||||
readAction,
|
||||
createAction,
|
||||
modifyAction,
|
||||
updateAction,
|
||||
removeAction
|
||||
];
|
||||
|
||||
@deprecated
|
||||
const List<String> ACTIONS = actions;
|
||||
|
||||
/// The standard Angel service events.
|
||||
const List<String> events = const <String>[
|
||||
indexedEvent,
|
||||
readEvent,
|
||||
createdEvent,
|
||||
modifiedEvent,
|
||||
updatedEvent,
|
||||
removedEvent
|
||||
];
|
||||
|
||||
@deprecated
|
||||
const List<String> EVENTS = events;
|
50
packages/websocket/lib/flutter.dart
Normal file
50
packages/websocket/lib/flutter.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
/// Flutter-compatible WebSocket client library for the Angel framework.
|
||||
library angel_websocket.flutter;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart' as http;
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'base_websocket_client.dart';
|
||||
export 'package:angel_client/angel_client.dart';
|
||||
export 'angel_websocket.dart';
|
||||
|
||||
// final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||
|
||||
/// Queries an Angel server via WebSockets.
|
||||
class WebSockets extends BaseWebSocketClient {
|
||||
final List<WebSocketsService> _services = [];
|
||||
|
||||
WebSockets(baseUrl,
|
||||
{bool reconnectOnClose = true, Duration reconnectInterval})
|
||||
: super(new http.IOClient(), baseUrl,
|
||||
reconnectOnClose: reconnectOnClose,
|
||||
reconnectInterval: reconnectInterval);
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token'}) {
|
||||
throw new UnimplementedError(
|
||||
'Opening popup windows is not supported in the `dart:io` client.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
for (var service in _services) {
|
||||
service.close();
|
||||
}
|
||||
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WebSocketChannel> getConnectedWebSocket() async {
|
||||
var socket = await WebSocket.connect(websocketUri.toString(),
|
||||
headers: authToken?.isNotEmpty == true
|
||||
? {'Authorization': 'Bearer $authToken'}
|
||||
: {});
|
||||
return new IOWebSocketChannel(socket);
|
||||
}
|
||||
}
|
30
packages/websocket/lib/hooks.dart
Normal file
30
packages/websocket/lib/hooks.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
|
||||
/// Prevents a WebSocket event from being broadcasted, to any client from the given [provider].
|
||||
///
|
||||
/// [provider] can be a String, a [Provider], or an Iterable.
|
||||
/// If [provider] is `null`, any provider will be blocked.
|
||||
HookedServiceEventListener doNotBroadcast([provider]) {
|
||||
return (HookedServiceEvent e) {
|
||||
if (e.params != null && e.params.containsKey('provider')) {
|
||||
bool deny = false;
|
||||
Iterable providers = provider is Iterable ? provider : [provider];
|
||||
|
||||
for (var p in providers) {
|
||||
if (deny) break;
|
||||
|
||||
if (p is Providers) {
|
||||
deny = deny ||
|
||||
p == e.params['provider'] ||
|
||||
e.params['provider'] == p.via;
|
||||
} else if (p == null) {
|
||||
deny = true;
|
||||
} else
|
||||
deny =
|
||||
deny || (e.params['provider'] as Providers).via == p.toString();
|
||||
}
|
||||
|
||||
e.params['broadcast'] = false;
|
||||
}
|
||||
};
|
||||
}
|
66
packages/websocket/lib/io.dart
Normal file
66
packages/websocket/lib/io.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
/// Command-line WebSocket client library for the Angel framework.
|
||||
library angel_websocket.io;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:angel_client/angel_client.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart' as http;
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'base_websocket_client.dart';
|
||||
export 'package:angel_client/angel_client.dart';
|
||||
export 'angel_websocket.dart';
|
||||
|
||||
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||
|
||||
/// Queries an Angel server via WebSockets.
|
||||
class WebSockets extends BaseWebSocketClient {
|
||||
final List<IoWebSocketsService> _services = [];
|
||||
|
||||
WebSockets(baseUrl,
|
||||
{bool reconnectOnClose = true, Duration reconnectInterval})
|
||||
: super(new http.IOClient(), baseUrl,
|
||||
reconnectOnClose: reconnectOnClose,
|
||||
reconnectInterval: reconnectInterval);
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token'}) {
|
||||
throw new UnimplementedError(
|
||||
'Opening popup windows is not supported in the `dart:io` client.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
for (var service in _services) {
|
||||
service.close();
|
||||
}
|
||||
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WebSocketChannel> getConnectedWebSocket() async {
|
||||
var socket = await WebSocket.connect(websocketUri.toString(),
|
||||
headers: authToken?.isNotEmpty == true
|
||||
? {'Authorization': 'Bearer $authToken'}
|
||||
: {});
|
||||
return new IOWebSocketChannel(socket);
|
||||
}
|
||||
|
||||
@override
|
||||
IoWebSocketsService<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer<Data> deserializer}) {
|
||||
String uri = path.replaceAll(_straySlashes, '');
|
||||
return new IoWebSocketsService<Id, Data>(socket, this, uri, type);
|
||||
}
|
||||
}
|
||||
|
||||
class IoWebSocketsService<Id, Data> extends WebSocketsService<Id, Data> {
|
||||
final Type type;
|
||||
|
||||
IoWebSocketsService(
|
||||
WebSocketChannel socket, WebSockets app, String uri, this.type)
|
||||
: super(socket, app, uri);
|
||||
}
|
454
packages/websocket/lib/server.dart
Normal file
454
packages/websocket/lib/server.dart
Normal file
|
@ -0,0 +1,454 @@
|
|||
/// Server-side support for WebSockets.
|
||||
library angel_websocket.server;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:mirrors';
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel_framework/http2.dart';
|
||||
import 'package:merge_map/merge_map.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'angel_websocket.dart';
|
||||
import 'constants.dart';
|
||||
export 'angel_websocket.dart';
|
||||
|
||||
part 'websocket_context.dart';
|
||||
|
||||
part 'websocket_controller.dart';
|
||||
|
||||
typedef String WebSocketResponseSerializer(data);
|
||||
|
||||
/// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s.
|
||||
class AngelWebSocket {
|
||||
List<WebSocketContext> _clients = <WebSocketContext>[];
|
||||
final List<String> _servicesAlreadyWired = [];
|
||||
|
||||
final StreamController<WebSocketAction> _onAction =
|
||||
new StreamController<WebSocketAction>();
|
||||
final StreamController _onData = new StreamController();
|
||||
final StreamController<WebSocketContext> _onConnection =
|
||||
new StreamController<WebSocketContext>.broadcast();
|
||||
final StreamController<WebSocketContext> _onDisconnect =
|
||||
new StreamController<WebSocketContext>.broadcast();
|
||||
|
||||
final Angel app;
|
||||
|
||||
/// If this is not `true`, then all client-side service parameters will be
|
||||
/// discarded, other than `params['query']`.
|
||||
final bool allowClientParams;
|
||||
|
||||
/// An optional whitelist of allowed client origins, or [:null:].
|
||||
final List<String> allowedOrigins;
|
||||
|
||||
/// An optional whitelist of allowed client protocols, or [:null:].
|
||||
final List<String> allowedProtocols;
|
||||
|
||||
/// If `true`, then clients can authenticate their WebSockets by sending a valid JWT.
|
||||
final bool allowAuth;
|
||||
|
||||
/// Send error information across WebSockets, without including debug information..
|
||||
final bool sendErrors;
|
||||
|
||||
/// A list of clients currently connected to this server via WebSockets.
|
||||
List<WebSocketContext> get clients => new List.unmodifiable(_clients);
|
||||
|
||||
/// Services that have already been hooked to fire socket events.
|
||||
List<String> get servicesAlreadyWired =>
|
||||
new List.unmodifiable(_servicesAlreadyWired);
|
||||
|
||||
/// Used to notify other nodes of an event's firing. Good for scaled applications.
|
||||
final StreamChannel<WebSocketEvent> synchronizationChannel;
|
||||
|
||||
/// Fired on any [WebSocketAction].
|
||||
Stream<WebSocketAction> get onAction => _onAction.stream;
|
||||
|
||||
/// Fired whenever a WebSocket sends data.
|
||||
Stream get onData => _onData.stream;
|
||||
|
||||
/// Fired on incoming connections.
|
||||
Stream<WebSocketContext> get onConnection => _onConnection.stream;
|
||||
|
||||
/// Fired when a user disconnects.
|
||||
Stream<WebSocketContext> get onDisconnection => _onDisconnect.stream;
|
||||
|
||||
/// Serializes data to WebSockets.
|
||||
WebSocketResponseSerializer serializer;
|
||||
|
||||
/// Deserializes data from WebSockets.
|
||||
Function deserializer;
|
||||
|
||||
AngelWebSocket(this.app,
|
||||
{this.sendErrors = false,
|
||||
this.allowClientParams = false,
|
||||
this.allowAuth = true,
|
||||
this.synchronizationChannel,
|
||||
this.serializer,
|
||||
this.deserializer,
|
||||
this.allowedOrigins,
|
||||
this.allowedProtocols}) {
|
||||
if (serializer == null) serializer = json.encode;
|
||||
if (deserializer == null) deserializer = (params) => params;
|
||||
}
|
||||
|
||||
HookedServiceEventListener serviceHook(String path) {
|
||||
return (HookedServiceEvent e) async {
|
||||
if (e.params != null && e.params['broadcast'] == false) return;
|
||||
|
||||
var event = await transformEvent(e);
|
||||
event.eventName = "$path::${event.eventName}";
|
||||
|
||||
_filter(WebSocketContext socket) {
|
||||
if (e.service.configuration.containsKey('ws:filter'))
|
||||
return e.service.configuration['ws:filter'](e, socket);
|
||||
else if (e.params != null && e.params.containsKey('ws:filter'))
|
||||
return e.params['ws:filter'](e, socket);
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
await batchEvent(event, filter: _filter);
|
||||
};
|
||||
}
|
||||
|
||||
/// Slates an event to be dispatched.
|
||||
Future<void> batchEvent(WebSocketEvent event,
|
||||
{filter(WebSocketContext socket), bool notify = true}) async {
|
||||
// Default implementation will just immediately fire events
|
||||
_clients.forEach((client) async {
|
||||
dynamic result = true;
|
||||
if (filter != null) result = await filter(client);
|
||||
if (result == true) {
|
||||
client.channel.sink.add((serializer ?? json.encode)(event.toJson()));
|
||||
}
|
||||
});
|
||||
|
||||
if (synchronizationChannel != null && notify != false)
|
||||
synchronizationChannel.sink.add(event);
|
||||
}
|
||||
|
||||
/// Returns a list of events yet to be sent.
|
||||
Future<List<WebSocketEvent>> getBatchedEvents() async => [];
|
||||
|
||||
/// Responds to an incoming action on a WebSocket.
|
||||
Future handleAction(WebSocketAction action, WebSocketContext socket) async {
|
||||
var split = action.eventName.split("::");
|
||||
|
||||
if (split.length < 2) {
|
||||
socket.sendError(new AngelHttpException.badRequest());
|
||||
return null;
|
||||
}
|
||||
|
||||
var service = app.findService(split[0]);
|
||||
|
||||
if (service == null) {
|
||||
socket.sendError(new AngelHttpException.notFound(
|
||||
message: "No service \"${split[0]}\" exists."));
|
||||
return null;
|
||||
}
|
||||
|
||||
var actionName = split[1];
|
||||
|
||||
if (action.params is! Map) action.params = <String, dynamic>{};
|
||||
|
||||
if (allowClientParams != true) {
|
||||
if (action.params['query'] is Map)
|
||||
action.params = {'query': action.params['query']};
|
||||
else
|
||||
action.params = {};
|
||||
}
|
||||
|
||||
var params = mergeMap<String, dynamic>([
|
||||
((deserializer ?? (params) => params)(action.params))
|
||||
as Map<String, dynamic>,
|
||||
{
|
||||
"provider": Providers.websocket,
|
||||
'__requestctx': socket.request,
|
||||
'__responsectx': socket.response
|
||||
}
|
||||
]);
|
||||
|
||||
try {
|
||||
if (actionName == indexAction) {
|
||||
socket.send(
|
||||
"${split[0]}::" + indexedEvent, await service.index(params));
|
||||
return null;
|
||||
} else if (actionName == readAction) {
|
||||
socket.send(
|
||||
"${split[0]}::" + readEvent, await service.read(action.id, params));
|
||||
return null;
|
||||
} else if (actionName == createAction) {
|
||||
return new WebSocketEvent(
|
||||
eventName: "${split[0]}::" + createdEvent,
|
||||
data: await service.create(action.data, params));
|
||||
} else if (actionName == modifyAction) {
|
||||
return new WebSocketEvent(
|
||||
eventName: "${split[0]}::" + modifiedEvent,
|
||||
data: await service.modify(action.id, action.data, params));
|
||||
} else if (actionName == updateAction) {
|
||||
return new WebSocketEvent(
|
||||
eventName: "${split[0]}::" + updatedEvent,
|
||||
data: await service.update(action.id, action.data, params));
|
||||
} else if (actionName == removeAction) {
|
||||
return new WebSocketEvent(
|
||||
eventName: "${split[0]}::" + removedEvent,
|
||||
data: await service.remove(action.id, params));
|
||||
} else {
|
||||
socket.sendError(new AngelHttpException.methodNotAllowed(
|
||||
message: "Method Not Allowed: \"$actionName\""));
|
||||
return null;
|
||||
}
|
||||
} catch (e, st) {
|
||||
catchError(e, st, socket);
|
||||
}
|
||||
}
|
||||
|
||||
/// Authenticates a [WebSocketContext].
|
||||
Future handleAuth(WebSocketAction action, WebSocketContext socket) async {
|
||||
if (allowAuth != false &&
|
||||
action.eventName == authenticateAction &&
|
||||
action.params['query'] is Map &&
|
||||
action.params['query']['jwt'] is String) {
|
||||
try {
|
||||
var auth = socket.request.container.make<AngelAuth>();
|
||||
var jwt = action.params['query']['jwt'] as String;
|
||||
AuthToken token;
|
||||
|
||||
token = new AuthToken.validate(jwt, auth.hmac);
|
||||
var user = await auth.deserializer(token.userId);
|
||||
socket.request
|
||||
..container.registerSingleton<AuthToken>(token)
|
||||
..container.registerSingleton(user, as: user.runtimeType as Type);
|
||||
socket._onAuthenticated.add(null);
|
||||
socket.send(authenticatedEvent,
|
||||
{'token': token.serialize(auth.hmac), 'data': user});
|
||||
} catch (e, st) {
|
||||
catchError(e, st, socket);
|
||||
}
|
||||
} else {
|
||||
socket.sendError(new AngelHttpException.badRequest(
|
||||
message: 'No JWT provided for authentication.'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Hooks a service up to have its events broadcasted.
|
||||
hookupService(Pattern _path, HookedService service) {
|
||||
String path = _path.toString();
|
||||
service.after(
|
||||
[
|
||||
HookedServiceEvent.created,
|
||||
HookedServiceEvent.modified,
|
||||
HookedServiceEvent.updated,
|
||||
HookedServiceEvent.removed
|
||||
],
|
||||
serviceHook(path),
|
||||
);
|
||||
_servicesAlreadyWired.add(path);
|
||||
}
|
||||
|
||||
/// Runs before firing [onConnection].
|
||||
Future handleConnect(WebSocketContext socket) async {}
|
||||
|
||||
/// Handles incoming data from a WebSocket.
|
||||
handleData(WebSocketContext socket, data) async {
|
||||
try {
|
||||
socket._onData.add(data);
|
||||
var fromJson = json.decode(data.toString());
|
||||
var action = new WebSocketAction.fromJson(fromJson as Map);
|
||||
_onAction.add(action);
|
||||
|
||||
if (action.eventName == null ||
|
||||
action.eventName is! String ||
|
||||
action.eventName.isEmpty) {
|
||||
throw new AngelHttpException.badRequest();
|
||||
}
|
||||
|
||||
if (fromJson is Map && fromJson.containsKey("eventName")) {
|
||||
socket._onAction.add(new WebSocketAction.fromJson(fromJson));
|
||||
socket.on
|
||||
._getStreamForEvent(fromJson["eventName"].toString())
|
||||
.add(fromJson["data"] as Map);
|
||||
}
|
||||
|
||||
if (action.eventName == authenticateAction)
|
||||
await handleAuth(action, socket);
|
||||
|
||||
if (action.eventName.contains("::")) {
|
||||
var split = action.eventName.split("::");
|
||||
|
||||
if (split.length >= 2) {
|
||||
if (actions.contains(split[1])) {
|
||||
var event = await handleAction(action, socket);
|
||||
if (event is Future) event = await event;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
catchError(e, st, socket);
|
||||
}
|
||||
}
|
||||
|
||||
void catchError(e, StackTrace st, WebSocketContext socket) {
|
||||
// Send an error
|
||||
if (e is AngelHttpException) {
|
||||
socket.sendError(e);
|
||||
app.logger?.severe(e.message, e.error ?? e, e.stackTrace);
|
||||
} else if (sendErrors) {
|
||||
var err = new AngelHttpException(e,
|
||||
message: e.toString(), stackTrace: st, errors: [st.toString()]);
|
||||
socket.sendError(err);
|
||||
app.logger?.severe(err.message, e, st);
|
||||
} else {
|
||||
var err = new AngelHttpException(e);
|
||||
socket.sendError(err);
|
||||
app.logger?.severe(e.toString(), e, st);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms a [HookedServiceEvent], so that it can be broadcasted.
|
||||
Future<WebSocketEvent> transformEvent(HookedServiceEvent event) async {
|
||||
return new WebSocketEvent(eventName: event.eventName, data: event.result);
|
||||
}
|
||||
|
||||
/// Hooks any [HookedService]s that are not being broadcasted yet.
|
||||
wireAllServices(Angel app) {
|
||||
for (Pattern key in app.services.keys.where((x) {
|
||||
return !_servicesAlreadyWired.contains(x) &&
|
||||
app.services[x] is HookedService;
|
||||
})) {
|
||||
hookupService(key, app.services[key] as HookedService);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures an [Angel] instance to listen for WebSocket connections.
|
||||
Future configureServer(Angel app) async {
|
||||
app..container.registerSingleton(this);
|
||||
|
||||
if (runtimeType != AngelWebSocket)
|
||||
app..container.registerSingleton<AngelWebSocket>(this);
|
||||
|
||||
// Set up services
|
||||
wireAllServices(app);
|
||||
|
||||
app.onService.listen((_) {
|
||||
wireAllServices(app);
|
||||
});
|
||||
|
||||
if (synchronizationChannel != null) {
|
||||
synchronizationChannel.stream.listen((e) => batchEvent(e, notify: false));
|
||||
}
|
||||
|
||||
app.shutdownHooks.add((_) => synchronizationChannel?.sink?.close());
|
||||
}
|
||||
|
||||
/// Handles an incoming [WebSocketContext].
|
||||
Future<void> handleClient(WebSocketContext socket) async {
|
||||
var origin = socket.request.headers.value('origin');
|
||||
if (allowedOrigins != null && !allowedOrigins.contains(origin)) {
|
||||
throw new AngelHttpException.forbidden(
|
||||
message:
|
||||
'WebSocket connections are not allowed from the origin "$origin".');
|
||||
}
|
||||
|
||||
_clients.add(socket);
|
||||
await handleConnect(socket);
|
||||
|
||||
_onConnection.add(socket);
|
||||
|
||||
socket.request.container.registerSingleton<WebSocketContext>(socket);
|
||||
|
||||
socket.channel.stream.listen(
|
||||
(data) {
|
||||
_onData.add(data);
|
||||
handleData(socket, data);
|
||||
},
|
||||
onDone: () {
|
||||
_onDisconnect.add(socket);
|
||||
_clients.remove(socket);
|
||||
},
|
||||
onError: (e) {
|
||||
_onDisconnect.add(socket);
|
||||
_clients.remove(socket);
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles an incoming HTTP request.
|
||||
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
||||
if (req is HttpRequestContext && res is HttpResponseContext) {
|
||||
if (!WebSocketTransformer.isUpgradeRequest(req.rawRequest))
|
||||
throw new AngelHttpException.badRequest();
|
||||
await res.detach();
|
||||
var ws = await WebSocketTransformer.upgrade(req.rawRequest);
|
||||
var channel = new IOWebSocketChannel(ws);
|
||||
var socket = new WebSocketContext(channel, req, res);
|
||||
scheduleMicrotask(() => handleClient(socket));
|
||||
return false;
|
||||
} else if (req is Http2RequestContext && res is Http2ResponseContext) {
|
||||
var connection =
|
||||
req.headers['connection']?.map((s) => s.toLowerCase().trim());
|
||||
var upgrade = req.headers.value('upgrade')?.toLowerCase();
|
||||
var version = req.headers.value('sec-websocket-version');
|
||||
var key = req.headers.value('sec-websocket-key');
|
||||
var protocol = req.headers.value('sec-websocket-protocol');
|
||||
|
||||
if (connection == null) {
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: 'Missing `connection` header.');
|
||||
} else if (!connection.contains('upgrade')) {
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: 'Missing "upgrade" in `connection` header.');
|
||||
} else if (upgrade != 'websocket') {
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: 'The `upgrade` header must equal "websocket".');
|
||||
} else if (version != '13') {
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: 'The `sec-websocket-version` header must equal "13".');
|
||||
} else if (key == null) {
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: 'Missing `sec-websocket-key` header.');
|
||||
} else if (protocol != null &&
|
||||
allowedProtocols != null &&
|
||||
!allowedProtocols.contains(protocol)) {
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: 'Disallowed `sec-websocket-protocol` header "$protocol".');
|
||||
} else {
|
||||
var stream = res.detach();
|
||||
var ctrl = new StreamChannelController<List<int>>();
|
||||
|
||||
ctrl.local.stream.listen((buf) {
|
||||
stream.sendData(buf);
|
||||
}, onDone: () {
|
||||
stream.outgoingMessages.close();
|
||||
});
|
||||
|
||||
if (req.hasParsedBody) {
|
||||
await ctrl.local.sink.close();
|
||||
} else {
|
||||
await req.body.pipe(ctrl.local.sink);
|
||||
}
|
||||
|
||||
var sink = utf8.encoder.startChunkedConversion(ctrl.foreign.sink);
|
||||
sink.add("HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: ${WebSocketChannel.signKey(key)}\r\n");
|
||||
if (protocol != null) sink.add("Sec-WebSocket-Protocol: $protocol\r\n");
|
||||
sink.add("\r\n");
|
||||
|
||||
var ws = new WebSocketChannel(ctrl.foreign);
|
||||
var socket = new WebSocketContext(ws, req, res);
|
||||
scheduleMicrotask(() => handleClient(socket));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
throw new ArgumentError(
|
||||
'Not an HTTP/1.1 or HTTP/2 RequestContext+ResponseContext pair: $req, $res');
|
||||
}
|
||||
}
|
||||
}
|
73
packages/websocket/lib/websocket_context.dart
Normal file
73
packages/websocket/lib/websocket_context.dart
Normal file
|
@ -0,0 +1,73 @@
|
|||
part of angel_websocket.server;
|
||||
|
||||
/// Represents a WebSocket session, with the original
|
||||
/// [RequestContext] and [ResponseContext] attached.
|
||||
class WebSocketContext {
|
||||
/// Use this to listen for events.
|
||||
_WebSocketEventTable on = new _WebSocketEventTable();
|
||||
|
||||
/// The underlying [StreamChannel].
|
||||
final StreamChannel channel;
|
||||
|
||||
/// The original [RequestContext].
|
||||
final RequestContext request;
|
||||
|
||||
/// The original [ResponseContext].
|
||||
final ResponseContext response;
|
||||
|
||||
StreamController<WebSocketAction> _onAction =
|
||||
new StreamController<WebSocketAction>();
|
||||
|
||||
StreamController<void> _onAuthenticated = StreamController();
|
||||
|
||||
StreamController<Null> _onClose = new StreamController<Null>();
|
||||
|
||||
StreamController _onData = new StreamController();
|
||||
|
||||
/// Fired on any [WebSocketAction];
|
||||
Stream<WebSocketAction> get onAction => _onAction.stream;
|
||||
|
||||
/// Fired when the user authenticates.
|
||||
Stream<void> get onAuthenticated => _onAuthenticated.stream;
|
||||
|
||||
/// Fired once the underlying [WebSocket] closes.
|
||||
Stream<Null> get onClose => _onClose.stream;
|
||||
|
||||
/// Fired when any data is sent through [channel].
|
||||
Stream get onData => _onData.stream;
|
||||
|
||||
WebSocketContext(this.channel, this.request, this.response);
|
||||
|
||||
/// Closes the underlying [StreamChannel].
|
||||
Future close() async {
|
||||
scheduleMicrotask(() async {
|
||||
await channel.sink.close();
|
||||
await _onAction.close();
|
||||
await _onAuthenticated.close();
|
||||
await _onData.close();
|
||||
await _onClose.add(null);
|
||||
await _onClose.close();
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends an arbitrary [WebSocketEvent];
|
||||
void send(String eventName, data) {
|
||||
channel.sink.add(json
|
||||
.encode(new WebSocketEvent(eventName: eventName, data: data).toJson()));
|
||||
}
|
||||
|
||||
/// Sends an error event.
|
||||
void sendError(AngelHttpException error) => send(errorEvent, error.toJson());
|
||||
}
|
||||
|
||||
class _WebSocketEventTable {
|
||||
Map<String, StreamController<Map>> _handlers = {};
|
||||
|
||||
StreamController<Map> _getStreamForEvent(String eventName) {
|
||||
if (!_handlers.containsKey(eventName))
|
||||
_handlers[eventName] = new StreamController<Map>();
|
||||
return _handlers[eventName];
|
||||
}
|
||||
|
||||
Stream<Map> operator [](String key) => _getStreamForEvent(key).stream;
|
||||
}
|
89
packages/websocket/lib/websocket_controller.dart
Normal file
89
packages/websocket/lib/websocket_controller.dart
Normal file
|
@ -0,0 +1,89 @@
|
|||
part of angel_websocket.server;
|
||||
|
||||
/// Marks a method as available to WebSockets.
|
||||
class ExposeWs {
|
||||
final String eventName;
|
||||
|
||||
const ExposeWs(this.eventName);
|
||||
}
|
||||
|
||||
/// A special controller that also supports WebSockets.
|
||||
class WebSocketController extends Controller {
|
||||
/// The plug-in instance powering this controller.
|
||||
final AngelWebSocket ws;
|
||||
|
||||
Map<String, MethodMirror> _handlers = {};
|
||||
Map<String, Symbol> _handlerSymbols = {};
|
||||
|
||||
WebSocketController(this.ws) : super();
|
||||
|
||||
/// Sends an event to all clients.
|
||||
void broadcast(String eventName, data, {filter(WebSocketContext socket)}) {
|
||||
ws.batchEvent(new WebSocketEvent(eventName: eventName, data: data),
|
||||
filter: filter);
|
||||
}
|
||||
|
||||
/// Fired on new connections.
|
||||
onConnect(WebSocketContext socket) {}
|
||||
|
||||
/// Fired on disconnections.
|
||||
onDisconnect(WebSocketContext socket) {}
|
||||
|
||||
/// Fired on all incoming actions.
|
||||
onAction(WebSocketAction action, WebSocketContext socket) async {}
|
||||
|
||||
/// Fired on arbitrary incoming data.
|
||||
onData(data, WebSocketContext socket) {}
|
||||
|
||||
@override
|
||||
Future configureServer(Angel app) async {
|
||||
if (findExpose(app.container.reflector) != null)
|
||||
await super.configureServer(app);
|
||||
|
||||
InstanceMirror instanceMirror = reflect(this);
|
||||
ClassMirror classMirror = reflectClass(this.runtimeType);
|
||||
classMirror.instanceMembers.forEach((sym, mirror) {
|
||||
if (mirror.isRegularMethod) {
|
||||
InstanceMirror exposeMirror = mirror.metadata.firstWhere(
|
||||
(mirror) => mirror.reflectee is ExposeWs,
|
||||
orElse: () => null);
|
||||
|
||||
if (exposeMirror != null) {
|
||||
ExposeWs exposeWs = exposeMirror.reflectee as ExposeWs;
|
||||
_handlers[exposeWs.eventName] = mirror;
|
||||
_handlerSymbols[exposeWs.eventName] = sym;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.onConnection.listen((socket) async {
|
||||
if (!socket.request.container.has<WebSocketContext>()) {
|
||||
socket.request.container.registerSingleton<WebSocketContext>(socket);
|
||||
}
|
||||
|
||||
await onConnect(socket);
|
||||
|
||||
socket.onData.listen((data) => onData(data, socket));
|
||||
|
||||
socket.onAction.listen((WebSocketAction action) async {
|
||||
var container = socket.request.container.createChild();
|
||||
container.registerSingleton<WebSocketAction>(action);
|
||||
|
||||
try {
|
||||
await onAction(action, socket);
|
||||
|
||||
if (_handlers.containsKey(action.eventName)) {
|
||||
var methodMirror = _handlers[action.eventName];
|
||||
var fn = instanceMirror.getField(methodMirror.simpleName).reflectee;
|
||||
return app.runContained(
|
||||
fn as Function, socket.request, socket.response, container);
|
||||
}
|
||||
} catch (e, st) {
|
||||
ws.catchError(e, st, socket);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ws.onDisconnection.listen(onDisconnect);
|
||||
}
|
||||
}
|
23
packages/websocket/pubspec.yaml
Normal file
23
packages/websocket/pubspec.yaml
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: angel_websocket
|
||||
description: Support for using pkg:angel_client with WebSockets. Designed for Angel.
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <3.0.0"
|
||||
version: 2.0.3
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_websocket
|
||||
dependencies:
|
||||
angel_auth: ^2.0.0-alpha
|
||||
angel_client: ^2.0.0-alpha
|
||||
angel_framework: ^2.0.0-rc.0
|
||||
angel_http_exception: ^1.0.0
|
||||
http: ">=0.11.0 <0.13.0"
|
||||
merge_map: ^1.0.0
|
||||
meta: ^1.0.0
|
||||
stream_channel: ^2.0.0
|
||||
web_socket_channel: ^1.0.0
|
||||
dev_dependencies:
|
||||
angel_container: ^1.0.0-alpha
|
||||
angel_model: ^1.0.0
|
||||
logging: ^0.11.0
|
||||
pedantic: ^1.0.0
|
||||
test: ^1.0.0
|
63
packages/websocket/test/auth_test.dart
Normal file
63
packages/websocket/test/auth_test.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel_client/io.dart' as c;
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import "package:angel_framework/http.dart";
|
||||
import 'package:angel_websocket/io.dart' as c;
|
||||
import 'package:angel_websocket/server.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const Map<String, String> USER = const {'username': 'foo', 'password': 'bar'};
|
||||
|
||||
main() {
|
||||
Angel app;
|
||||
AngelHttp http;
|
||||
c.Angel client;
|
||||
c.WebSockets ws;
|
||||
|
||||
setUp(() async {
|
||||
app = new Angel();
|
||||
http = new AngelHttp(app, useZone: false);
|
||||
var auth = new AngelAuth();
|
||||
|
||||
auth.serializer = (_) async => 'baz';
|
||||
auth.deserializer = (_) async => USER;
|
||||
|
||||
auth.strategies['local'] = new LocalAuthStrategy(
|
||||
(username, password) async {
|
||||
if (username == 'foo' && password == 'bar') return USER;
|
||||
},
|
||||
);
|
||||
|
||||
app.post('/auth/local', auth.authenticate('local'));
|
||||
|
||||
await app.configure(auth.configureServer);
|
||||
var sock = new AngelWebSocket(app);
|
||||
await app.configure(sock.configureServer);
|
||||
app.all('/ws', sock.handleRequest);
|
||||
app.logger = new Logger('angel_auth')..onRecord.listen(print);
|
||||
|
||||
var server = await http.startServer();
|
||||
client = new c.Rest('http://${server.address.address}:${server.port}');
|
||||
ws = new c.WebSockets('ws://${server.address.address}:${server.port}/ws');
|
||||
await ws.connect();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
return Future.wait([
|
||||
http.close(),
|
||||
client.close(),
|
||||
ws.close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('auth event fires', () async {
|
||||
var localAuth = await client.authenticate(type: 'local', credentials: USER);
|
||||
print('JWT: ${localAuth.token}');
|
||||
|
||||
ws.authenticateViaJwt(localAuth.token);
|
||||
var auth = await ws.onAuthenticated.first;
|
||||
expect(auth.token, localAuth.token);
|
||||
});
|
||||
}
|
35
packages/websocket/test/controller/common.dart
Normal file
35
packages/websocket/test/controller/common.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_websocket/server.dart';
|
||||
|
||||
class Game {
|
||||
final String playerOne, playerTwo;
|
||||
|
||||
const Game({this.playerOne, this.playerTwo});
|
||||
|
||||
factory Game.fromJson(Map data) => new Game(
|
||||
playerOne: data['playerOne'].toString(),
|
||||
playerTwo: data['playerTwo'].toString());
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'playerOne': playerOne, 'playerTwo': playerTwo};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is Game &&
|
||||
other.playerOne == playerOne &&
|
||||
other.playerTwo == playerTwo;
|
||||
}
|
||||
|
||||
const Game johnVsBob = const Game(playerOne: 'John', playerTwo: 'Bob');
|
||||
|
||||
@Expose('/game')
|
||||
class GameController extends WebSocketController {
|
||||
GameController(AngelWebSocket ws) : super(ws);
|
||||
|
||||
@ExposeWs('search')
|
||||
search(WebSocketContext socket) async {
|
||||
print('User is searching for a game...');
|
||||
socket.send('searched', johnVsBob);
|
||||
}
|
||||
}
|
70
packages/websocket/test/controller/io_test.dart
Normal file
70
packages/websocket/test/controller/io_test.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_container/mirrors.dart';
|
||||
import 'package:angel_framework/angel_framework.dart' as srv;
|
||||
import "package:angel_framework/http.dart" as srv;
|
||||
import 'package:angel_websocket/io.dart' as ws;
|
||||
import 'package:angel_websocket/server.dart' as srv;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
srv.Angel app;
|
||||
srv.AngelHttp http;
|
||||
ws.WebSockets client;
|
||||
srv.AngelWebSocket websockets;
|
||||
HttpServer server;
|
||||
String url;
|
||||
|
||||
setUp(() async {
|
||||
app = new srv.Angel(reflector: const MirrorsReflector());
|
||||
http = new srv.AngelHttp(app, useZone: false);
|
||||
|
||||
websockets = new srv.AngelWebSocket(app)
|
||||
..onData.listen((data) {
|
||||
print('Received by server: $data');
|
||||
});
|
||||
|
||||
await app.configure(websockets.configureServer);
|
||||
app.all('/ws', websockets.handleRequest);
|
||||
await app.configure(new GameController(websockets).configureServer);
|
||||
app.logger = new Logger('angel_auth')..onRecord.listen(print);
|
||||
|
||||
server = await http.startServer();
|
||||
url = 'ws://${server.address.address}:${server.port}/ws';
|
||||
|
||||
client = new ws.WebSockets(url);
|
||||
await client.connect(timeout: new Duration(seconds: 3));
|
||||
|
||||
print('Connected');
|
||||
|
||||
client
|
||||
..onData.listen((data) {
|
||||
print('Received by client: $data');
|
||||
})
|
||||
..onError.listen((error) {
|
||||
// Auto-fail tests on errors ;)
|
||||
stderr.writeln(error);
|
||||
error.errors.forEach(stderr.writeln);
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await client.close();
|
||||
await http.close();
|
||||
app = null;
|
||||
client = null;
|
||||
server = null;
|
||||
url = null;
|
||||
});
|
||||
|
||||
group('controller.io', () {
|
||||
test('search', () async {
|
||||
client.sendAction(new ws.WebSocketAction(eventName: 'search'));
|
||||
var search = await client.on['searched'].first;
|
||||
print('Searched: ${search.data}');
|
||||
expect(new Game.fromJson(search.data as Map), equals(johnVsBob));
|
||||
});
|
||||
});
|
||||
}
|
5
packages/websocket/test/service/browser_test.dart
Normal file
5
packages/websocket/test/service/browser_test.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
group('service.browser', () {});
|
||||
}
|
35
packages/websocket/test/service/common.dart
Normal file
35
packages/websocket/test/service/common.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_model/angel_model.dart';
|
||||
import 'package:angel_websocket/base_websocket_client.dart';
|
||||
import 'package:angel_websocket/server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class Todo extends Model {
|
||||
String text;
|
||||
String when;
|
||||
|
||||
Todo({String this.text, String this.when});
|
||||
}
|
||||
|
||||
class TodoService extends MapService {
|
||||
TodoService() : super() {
|
||||
configuration['ws:filter'] =
|
||||
(HookedServiceEvent e, WebSocketContext socket) {
|
||||
print('Hello, service filter world!');
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
testIndex(BaseWebSocketClient client) async {
|
||||
var todoService = client.service('api/todos');
|
||||
scheduleMicrotask(() => todoService.index());
|
||||
|
||||
var indexed = await todoService.onIndexed.first;
|
||||
print('indexed: $indexed');
|
||||
|
||||
expect(indexed, isList);
|
||||
expect(indexed, isEmpty);
|
||||
}
|
60
packages/websocket/test/service/io_test.dart
Normal file
60
packages/websocket/test/service/io_test.dart
Normal file
|
@ -0,0 +1,60 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart' as srv;
|
||||
import "package:angel_framework/http.dart" as srv;
|
||||
import 'package:angel_websocket/io.dart' as ws;
|
||||
import 'package:angel_websocket/server.dart' as srv;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
srv.Angel app;
|
||||
srv.AngelHttp http;
|
||||
ws.WebSockets client;
|
||||
srv.AngelWebSocket websockets;
|
||||
HttpServer server;
|
||||
String url;
|
||||
|
||||
setUp(() async {
|
||||
app = new srv.Angel()..use('/api/todos', new TodoService());
|
||||
http = new srv.AngelHttp(app, useZone: false);
|
||||
|
||||
websockets = new srv.AngelWebSocket(app)
|
||||
..onData.listen((data) {
|
||||
print('Received by server: $data');
|
||||
});
|
||||
|
||||
await app.configure(websockets.configureServer);
|
||||
app.all('/ws', websockets.handleRequest);
|
||||
app.logger = new Logger('angel_auth')..onRecord.listen(print);
|
||||
server = await http.startServer();
|
||||
url = 'ws://${server.address.address}:${server.port}/ws';
|
||||
|
||||
client = new ws.WebSockets(url);
|
||||
await client.connect();
|
||||
|
||||
client
|
||||
..onData.listen((data) {
|
||||
print('Received by client: $data');
|
||||
})
|
||||
..onError.listen((error) {
|
||||
// Auto-fail tests on errors ;)
|
||||
stderr.writeln(error);
|
||||
error.errors.forEach(stderr.writeln);
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await client.close();
|
||||
await http.close();
|
||||
app = null;
|
||||
client = null;
|
||||
server = null;
|
||||
url = null;
|
||||
});
|
||||
|
||||
group('service.io', () {
|
||||
test('index', () => testIndex(client));
|
||||
});
|
||||
}
|
9
packages/websocket/web/index.html
Normal file
9
packages/websocket/web/index.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Client</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.dart.js"></script>
|
||||
</body>
|
||||
</html>
|
12
packages/websocket/web/main.dart
Normal file
12
packages/websocket/web/main.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'dart:html';
|
||||
import 'package:angel_websocket/browser.dart';
|
||||
|
||||
/// Dummy app to ensure client works with DDC.
|
||||
main() {
|
||||
var app = new WebSockets(window.location.origin);
|
||||
window.alert(app.baseUrl.toString());
|
||||
|
||||
app.connect().catchError((_) {
|
||||
window.alert('no websocket');
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue