master
garrettmills 5 years ago
parent 68fb47f936
commit 74252352a6

@ -0,0 +1,175 @@
# flitter-socket
`flitter-socket` is a transactional-websocket implementation for Flitter. For general-purpose applications that want real-time data, a websocket is awesome. But, that data may not always have the same structure. `flitter-socket` adds a transactional layer to `express-ws` to allow clients to make live requests to the server, and (the true attraction of this system) _the server to make live requests to the client_. All of this, while still fitting reasonably well within Flitter's existing controller framework.
## Getting Started
### Installation
`flitter-socket` doesn't ship with Flitter by default, but it's pretty easy to add. First, install the library:
```shell
yarn add flitter-socket
```
Then, add the following line to the "Custom Units" section of your application's `Units.flitter.js` file:
```javascript
'Socket' : new (require('flitter-socket/SocketUnit'))(),
```
Now, you should be able to launch Flitter with the sockets unit:
```shell
$ ./flitter shell
(flitter)> _flitter.has('sockets')
true
```
### Defining Socket Routes - Key Concepts
Sockets in Flitter don't work the same as normal requests. With normal requests, you define one route per possible endpoint, and when an HTTP request comes in to that endpoint, Flitter calls the controller method specified in the routes definition file.
However, because a websocket connection _begins_ with an HTTP connection, but then stays open and may make many more requests/responses, it's handled differently by `flitter-socket`. The general flow is as follows:
* Define a websocket controller class with various methods.
* In a routing file's socket definition, define the route used as the connection endpoint and point it to the socket controller's special, built-in `_connect` method. This sets up a connection manager that can handle 2-way transactions between the client and the server.
* Requests from the client specify endpoints that correspond to method names on the socket controller. Those methods are called when a valid client request is received.
* Requests from the server to the client can be made using the controller's built-in `_request` method.
So, we're going to first create a template controller that we'll come back to later, and now define the routes. Create the template controller like so:
```shell
./flitter new socket:controller SocketTest
```
Now, open any routes file (in our case, just we're just using `index.routes.js`) and add the following:
```javascript
socket: {
'/socket-test': [ _flitter.controller('SocketTest')._connect ],
},
```
As previously mentioned, `_connect` is a method of the `SocketController` class that bootstraps the incoming websocket to support `flitter-socket`'s transactional protocol.
### Socket Controllers - Key Concepts
Socket controllers define the methods and logic available to open websocket connections. They are responsible for sending and processing transactions with connected clients. Let's look at the template controller we generated, `SocketController.controller.js`:
```javascript
const SocketController = require('flitter-socket/Controller')
class SocketTest extends SocketController {
ping(transaction, socket){
console.log('Sending Ping!')
transaction.status(200).message('Pinging!').send(transaction.incoming)
// Make a request to the client
this._request('testendp', {hello: 'world'}, (t, ws, data) => {
console.log('Got client response!')
console.log(data)
t.resolved = true
}, transaction.connection_id)
}
}
module.exports = exports = SocketTest
```
The `flitter-socket/SocketController` superclass provides some helper methods for bootstrapping and managing websocket connections. It also validates requests to conform to the `flitter-socket` spec, and creates `flitter-socket/Transaction` instances.
In this controller, there's one endpoint specified, `ping`. This is an endpoint that can be called by requests that come in from the client. It is passed 2 arguments: an instance of `flitter-socket/ClientServerTransaction`, and the open websocket. As much as possible, you should interact with websocket clients through the transaction instance, not the socket directly.
This simple endpoint does two things. First, it sends a response back to the client with any incoming data the request contained and the message "Pinging!". Then, it makes a request to the client. (Note that this request is a separate transaction from the one we just processed.)
The request is made to the `testendp` endpoint on the **client** and the client is sent `{hello: 'world'}` as data. The third argument is the callback method which is called when the client sends a valid response to the server's request. The last argument is the specific connection ID to whom the request should be sent. This is necessary because a controller may be managing many open websocket connections at once.
The callback function is passed 3 arguments, similar to before: an instance of `flitter-socket/ServerClientTransaction`, the open websocket, and the response data. In the callback, we mark the transaction as resolved once we have finished processing the response.
### SocketController Helper Methods
See the [full docs](https://flitter.garrettmills.dev/) fore more info.
## `flitter-socket` Data Specification
So, we now know how to define endpoints and logic for incoming client connections, but how do we actually _connect_ a client to begin with? `flitter-socket` imposes a strict structure on websocket connections to enable this 2-way transactional processing. This means that clients should send and keep track of transactions in a particular way.
> Flitter provides a simple client-side implementation of this spec to make interacting with `flitter-socket` servers simpler. [Check it out here.](https://git.garrettmills.dev/flitter/socket-client-js)
### Client-to-Server Requests
Let's look at an example of a request for data made from the client to the server:
```json
{
"transaction_id": "e0b193dc-2e33-49df-9fcd-7dde479a645b",
"type": "request",
"endpoint": "ping",
"data": { "hi": "there" }
}
```
There are several important parts to this transaction. Let's break down all possible fields:
- First, every message sent to or from a `flitter-socket` compliant connection _must_ be a valid JSON object.
- Every message should be a JSON object which contains some or all of the following:
- `transaction_id` (**required**) - every transaction MUST have a universally-unique tranaction ID. This ID must be unique not only to the client-side, but also to the server. Therefore, it is recommended that you use a UUID library like `uuid` to generate these. The `flitter-socket` server implementation uses `uuid/v4`. This field is how the connection managers on either side of the connection match up requests with responses to call the appropriate handlers.
- `type` (**required**) - either "request" or "response" - this specifies the type of data that is being sent. A request is a message that the sender is awaiting data from the sender. A response is a message that the sender is fulfilling that data.
- `endpoint` (**required for request type**) - If the message is a request, this specifies the endpoint that should be used to handle the request. In this example, it would call the `ping()` method on our `SocketTest` controller.
- `status` (**required for response type**) - If the message is a response, specifies the HTTP status code-equivalent of the result of the transaction. For most successful cases, this should be `200`.
- `data` (_optional_) - The data payload included in the message. This may be request parameters or response data.
Sending this request to the server we set up above would produce the following response:
```json
{
"status":200,
"transaction_id":"e0b193dc-2e33-49df-9fcd-7dde479a645b",
"type":"response",
"message":"Pinging!",
"data":{"hi":"there"}
}
```
### Server-to-Client Transactions
Server-to-Client transactions represent requests from the server to the client. These are identical in every respect to Client-to-Server transactions, but the client should be configured to handle these appropriately.
### Message Validation
Any compliant server that can receive `flitter-socket` transaction messages should validate and reject invalid messages in a particular format. For the server-side implementation, this is done via an instance of the `flitter-socket/ClientErrorTransaction` class.
If possible, the transaction ID should be sent back to the client with these responses. Here's an example of a response to a message that failed validation. Say we sent the following request:
```json
{
"transaction_id": "e0b193dc-2e33-49df-9fcd-7dde479a645b",
"type": "request",
"data": { "hi": "there" }
}
```
This contains everything `flitter-socket` needs, except an endpoint to process the request with. This should generate the following response from the recipient:
```json
{
"status":400,
"transaction_id":"e0b193dc-2e33-49df-9fcd-7dde479a645b",
"type":"response",
"message":"Incoming request message must include a valid endpoint.",
"data":{}
}
```
Note that the equivalent HTTP response code was set properly. `message` can be arbitrary, but should be clear enough that it is obvious to the client why the request was rejected. In all possible instances, the `transaction_id` should be send back with these rejections. In fact, there are only two acceptable instances when it may be omitted:
1. If no transaction ID was provided by the client.
2. If the message could not be parsed as valid JSON.
In either of these cases, the response's `transaction_id` field should be set to `"unknown"`:
```json
{
"status":400,
"transaction_id":"unknown",
"type":"response",
"message":"Incoming message must be valid FSP JSON object.",
"data":{}
}
```
## The `socket-client-js` Library
Flitter provides a basic client-side implementation of this spec to make interacting with `flitter-socket` servers easier. [More info here.](https://git.garrettmills.dev/flitter/socket-client-js)
## License (MIT)
`flitter-socket`
Copyright © 2019 Garrett Mills
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.
Loading…
Cancel
Save