2021-05-15 11:20:33 +00:00
|
|
|
# angel3_pub_sub
|
2021-05-18 14:47:56 +00:00
|
|
|
[![version](https://img.shields.io/badge/pub-v3.0.2-brightgreen)](https://pub.dartlang.org/packages/angel3_pub_sub)
|
2021-05-15 11:20:33 +00:00
|
|
|
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
|
|
|
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
|
|
|
|
|
|
|
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/pub_sub/LICENSE)
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
Keep application instances in sync with a simple pub/sub API.
|
|
|
|
|
|
|
|
# Installation
|
2021-05-15 11:20:33 +00:00
|
|
|
Add `angel3_pub_sub` as a dependency in your `pubspec.yaml` file:
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
```yaml
|
|
|
|
dependencies:
|
2021-05-15 11:20:33 +00:00
|
|
|
angel3_pub_sub: ^3.0.0
|
2021-03-08 12:56:39 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
Then, be sure to run `pub get` in your terminal.
|
|
|
|
|
|
|
|
# Usage
|
2021-05-15 11:20:33 +00:00
|
|
|
`pub_sub` is your typical pub/sub API. However, `angel3_pub_sub` enforces authentication of every
|
|
|
|
request. It is very possible that `angel3_pub_sub` will run on both servers and in the browser,
|
|
|
|
or on a platform angel3_pub_sublike Flutter. Thus, there are provisions available to limit
|
2021-03-08 12:56:39 +00:00
|
|
|
access.
|
|
|
|
|
2021-05-15 11:20:33 +00:00
|
|
|
**Be careful to not leak any `angel3_pub_sub` client ID's if operating over a network.**
|
2021-03-08 12:56:39 +00:00
|
|
|
If you do, you risk malicious users injecting events into your application, which
|
|
|
|
could ultimately spell *disaster*.
|
|
|
|
|
2021-05-15 11:20:33 +00:00
|
|
|
A `angel3_pub_sub` server can operate across multiple *adapters*, which take care of interfacing data over different
|
2021-03-08 12:56:39 +00:00
|
|
|
media. For example, a single server can handle pub/sub between multiple Isolates and TCP Sockets, as well as
|
|
|
|
WebSockets, simultaneously.
|
|
|
|
|
|
|
|
```dart
|
2021-05-15 11:20:33 +00:00
|
|
|
import 'package:angel3_pub_sub/angel3_pub_sub.dart' as pub_sub;
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
main() async {
|
2021-05-15 11:20:33 +00:00
|
|
|
var server = pub_sub.Server([
|
|
|
|
FooAdapter(...),
|
|
|
|
BarAdapter(...)
|
2021-03-08 12:56:39 +00:00
|
|
|
]);
|
|
|
|
|
2021-05-15 11:20:33 +00:00
|
|
|
server.addAdapter( BazAdapter(...));
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
// Call `start` to activate adapters, and begin handling requests.
|
|
|
|
server.start();
|
|
|
|
}
|
|
|
|
```
|
|
|
|
### Trusted Clients
|
2021-05-15 11:20:33 +00:00
|
|
|
You can use `package:angel3_pub_sub` without explicitly registering
|
2021-03-08 12:56:39 +00:00
|
|
|
clients, *if and only if* those clients come from trusted sources.
|
|
|
|
|
|
|
|
Clients via `Isolate` are always trusted.
|
|
|
|
|
|
|
|
Clients via `package:json_rpc_2` must be explicitly marked
|
|
|
|
as trusted (i.e. using an IP whitelist mechanism):
|
|
|
|
|
|
|
|
```dart
|
2021-05-15 11:20:33 +00:00
|
|
|
JsonRpc2Adapter(..., isTrusted: false);
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
// Pass `null` as Client ID when trusted...
|
2021-05-15 11:20:33 +00:00
|
|
|
pub_sub.IsolateClient(null);
|
2021-03-08 12:56:39 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
### Access Control
|
|
|
|
The ID's of all *untrusted* clients who will connect to the server must be known at start-up time.
|
|
|
|
You may not register new clients after the server has started. This is mostly a security consideration;
|
|
|
|
if it is impossible to register new clients, then malicious users cannot grant themselves additional
|
|
|
|
privileges within the system.
|
|
|
|
|
|
|
|
```dart
|
2021-05-15 11:20:33 +00:00
|
|
|
import 'package:angel3_pub_sub/angel3_pub_sub.dart' as pub_sub;
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
main() async {
|
|
|
|
// ...
|
|
|
|
server.registerClient(const ClientInfo('<client-id>'));
|
|
|
|
|
|
|
|
// Create a user who can subscribe, but not publish.
|
|
|
|
server.registerClient(const ClientInfo('<client-id>', canPublish: false));
|
|
|
|
|
|
|
|
// Create a user who can publish, but not subscribe.
|
|
|
|
server.registerClient(const ClientInfo('<client-id>', canSubscribe: false));
|
|
|
|
|
|
|
|
// Create a user with no privileges whatsoever.
|
|
|
|
server.registerClient(const ClientInfo('<client-id>', canPublish: false, canSubscribe: false));
|
|
|
|
|
|
|
|
server.start();
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
## Isolates
|
|
|
|
If you are just running multiple instances of a server,
|
2021-05-15 11:20:33 +00:00
|
|
|
use `package:angel3_pub_sub/isolate.dart`.
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
You'll need one isolate to be the master. Typically this is the first isolate you create.
|
|
|
|
|
|
|
|
```dart
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:isolate';
|
2021-05-15 11:20:33 +00:00
|
|
|
import 'package:angel3_pub_sub/isolate.dart' as pub_sub;
|
|
|
|
import 'package:angel3_pub_sub/angel3_pub_sub.dart' as pub_sub;
|
2021-03-08 12:56:39 +00:00
|
|
|
|
2021-05-15 11:20:33 +00:00
|
|
|
void main() async {
|
2021-03-08 12:56:39 +00:00
|
|
|
// Easily bring up a server.
|
2021-05-15 11:20:33 +00:00
|
|
|
var adapter = pub_sub.IsolateAdapter();
|
|
|
|
var server = pub_sub.Server([adapter]);
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
// You then need to create a client that will connect to the adapter.
|
|
|
|
// Each isolate in your application should contain a client.
|
|
|
|
for (int i = 0; i < Platform.numberOfProcessors - 1; i++) {
|
2021-05-15 11:20:33 +00:00
|
|
|
server.registerClient(pub_sub.ClientInfo('client$i'));
|
2021-03-08 12:56:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Start the server.
|
|
|
|
server.start();
|
|
|
|
|
|
|
|
// Next, let's start isolates that interact with the server.
|
|
|
|
//
|
|
|
|
// Fortunately, we can send SendPorts over Isolates, so this is no hassle.
|
|
|
|
for (int i = 0; i < Platform.numberOfProcessors - 1; i++)
|
|
|
|
Isolate.spawn(isolateMain, [i, adapter.receivePort.sendPort]);
|
|
|
|
|
|
|
|
// It's possible that you're running your application in the server isolate as well:
|
|
|
|
isolateMain([0, adapter.receivePort.sendPort]);
|
|
|
|
}
|
|
|
|
|
|
|
|
void isolateMain(List args) {
|
|
|
|
var client =
|
2021-05-15 11:20:33 +00:00
|
|
|
pub_sub.IsolateClient('client${args[0]}', args[1] as SendPort);
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
// The client will connect automatically. In the meantime, we can start subscribing to events.
|
|
|
|
client.subscribe('user::logged_in').then((sub) {
|
|
|
|
// The `ClientSubscription` class extends `Stream`. Hooray for asynchrony!
|
|
|
|
sub.listen((msg) {
|
|
|
|
print('Logged in: $msg');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
## JSON RPC 2.0
|
|
|
|
If you are not running on isolates, you need to import
|
2021-05-15 11:20:33 +00:00
|
|
|
`package:angel3_pub_sub/json_rpc_2.dart`. This library leverages `package:json_rpc_2` and
|
2021-03-08 12:56:39 +00:00
|
|
|
`package:stream_channel` to create clients and servers that can hypothetically run on any
|
|
|
|
medium, i.e. WebSockets, or TCP Sockets.
|
|
|
|
|
2021-05-15 11:20:33 +00:00
|
|
|
Check out `test/json_rpc_2_test.dart` for an example of serving `angel3_pub_sub` over TCP sockets.
|
2021-03-08 12:56:39 +00:00
|
|
|
|
|
|
|
# Protocol
|
2021-05-15 11:20:33 +00:00
|
|
|
`angel3_pub_sub` is built upon a simple RPC, and this package includes
|
2021-03-08 12:56:39 +00:00
|
|
|
an implementation that runs via `SendPort`s and `ReceivePort`s, as
|
|
|
|
well as one that runs on any `StreamChannel<String>`.
|
|
|
|
|
|
|
|
Data sent over the wire looks like the following:
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// Sent by a client to initiate an exchange.
|
|
|
|
interface Request {
|
|
|
|
// This is an arbitrary string, assigned by your client, but in every case,
|
|
|
|
// the client uses this to match your requests with asynchronous responses.
|
|
|
|
request_id: string,
|
|
|
|
|
|
|
|
// The ID of the client to authenticate as.
|
|
|
|
//
|
|
|
|
// As you can imagine, this should be kept secret, to prevent breaches.
|
|
|
|
client_id: string,
|
|
|
|
|
|
|
|
// Required for *every* request.
|
|
|
|
params: {
|
|
|
|
// A value to be `publish`ed.
|
|
|
|
value?: any,
|
|
|
|
|
|
|
|
// The name of an event to `publish`.
|
|
|
|
event_name?: string,
|
|
|
|
|
|
|
|
// The ID of a subscription to be cancelled.
|
|
|
|
subscription_id?: string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sent by the server in response to a request.
|
|
|
|
interface Response {
|
|
|
|
// `true` for success, `false` for failures.
|
|
|
|
status: boolean,
|
|
|
|
|
|
|
|
// Only appears if `status` is `false`; explains why an operation failed.
|
|
|
|
error_message?: string,
|
|
|
|
|
|
|
|
// Matches the request_id sent by the client.
|
|
|
|
request_id: string,
|
|
|
|
|
|
|
|
result?: {
|
|
|
|
// The number of other clients to whom an event was `publish`ed.
|
|
|
|
listeners:? number,
|
|
|
|
|
|
|
|
// The ID of a created subscription.
|
|
|
|
subscription_id?: string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
When sending via JSON_RPC 2.0, the `params` of a `Request` are simply folded into the object
|
|
|
|
itself, for simplicity's sake. In this case, a response will be sent as a notification whose
|
|
|
|
name is the `request_id`.
|
|
|
|
|
|
|
|
In the case of Isolate clients/servers, events will be simply sent as Lists:
|
|
|
|
|
|
|
|
```dart
|
|
|
|
['<event-name>', value]
|
|
|
|
```
|
|
|
|
|
|
|
|
Clients can send the following (3) methods:
|
|
|
|
|
|
|
|
* `subscribe` (`event_name`:string): Subscribe to an event.
|
|
|
|
* `unsubscribe` (`subscription_id`:string): Unsubscribe from an event you previously subscribed to.
|
|
|
|
* `publish` (`event_name`:string, `value`:any): Publish an event to all other clients who are subscribed.
|
|
|
|
|
2021-05-15 11:20:33 +00:00
|
|
|
The client and server in `package:angel3_pub_sub/isolate.dart` must make extra
|
2021-03-08 12:56:39 +00:00
|
|
|
provisions to keep track of client ID's. Since `SendPort`s and `ReceivePort`s
|
|
|
|
do not have any sort of guaranteed-unique ID's, new clients must send their
|
|
|
|
`SendPort` to the server before sending any requests. The server then responds
|
|
|
|
with an `id` that must be used to identify a `SendPort` to send a response to.
|