chore: dox++
This commit is contained in:
parent
2c702d7b05
commit
9d3ab086e5
|
@ -12,13 +12,17 @@ export default async function flecksDocusaurus() {
|
|||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
...defaults,
|
||||
title: 'flecks',
|
||||
tagline: 'not static',
|
||||
favicon: 'flecks.png',
|
||||
url: 'https://cha0s.github.io',
|
||||
baseUrl: '/flecks/',
|
||||
favicon: 'flecks.png',
|
||||
tagline: 'not static',
|
||||
title: 'flecks',
|
||||
url: 'https://cha0s.github.io',
|
||||
markdown: {
|
||||
mermaid: true,
|
||||
},
|
||||
organizationName: 'cha0s', // Usually your GitHub org/user name.
|
||||
projectName: 'flecks', // Usually your repo name.
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
|
|
|
@ -28,5 +28,8 @@
|
|||
"@flecks/user": "*",
|
||||
"@flecks/web": "*",
|
||||
"lerna": "^7.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/theme-mermaid": "3.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,3 +2,351 @@
|
|||
title: Sockets
|
||||
description: Run a websocket server and define packets.
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Sockets
|
||||
|
||||
flecks provides real-time socket connections through [Socket.IO](https://socket.io/docs/v4/).
|
||||
Packets are binary by default so they are small and fast.
|
||||
|
||||
## Packets
|
||||
|
||||
```javascript title="packages/example/src/index.js"
|
||||
import {Flecks} from '@flecks/core/server';
|
||||
|
||||
export const hooks = {
|
||||
'@flecks/socket.packets': Flecks.provide(require.context('./packets')),
|
||||
}
|
||||
```
|
||||
|
||||
The simplest packet can be defined like so:
|
||||
|
||||
```javascript title="packages/example/src/packets/slap.js"
|
||||
export default (flecks) => {
|
||||
const {Packet} = flecks.fleck('@flecks/socket');
|
||||
return class Slap extends Packet {};
|
||||
};
|
||||
```
|
||||
|
||||
This transmits no packet data at all, only the packet ID. e.g., you just know you've been
|
||||
slapped, since it's a slap packet. You don't know why, how hard, etc.
|
||||
|
||||
This is a **long-winded way** of sending a packet:
|
||||
|
||||
```javascript
|
||||
const {Slap} = flecks.socket.Packets;
|
||||
socket.send(new Slap());
|
||||
```
|
||||
|
||||
but let's learn about **packet hydration** to make working with packets a lot easier.
|
||||
|
||||
### Packet hydration
|
||||
|
||||
Instead of getting the packet class and having to manually create a new packet to send every time,
|
||||
we can instead send a **dehydrated packet**:
|
||||
|
||||
```javascript
|
||||
socket.send(['Slap']);
|
||||
```
|
||||
|
||||
Notice that **a dehydrated packet is an array**. The first element of the array is the
|
||||
[gathered name](./gathering) of the packet.
|
||||
|
||||
:::tip
|
||||
|
||||
The second element is the `data` passed to the packet constructor. We'll see a couple more
|
||||
examples of sending dehydrated packets on this page to come.
|
||||
|
||||
:::
|
||||
|
||||
Dehydrated sending is *just better* and should always be preferred!
|
||||
|
||||
### SchemaPack
|
||||
|
||||
Packets may implement a static getter method `data` to define the schema with which to serialize the
|
||||
packet data. See [SchemaPack](https://github.com/phretaddin/schemapack)
|
||||
to learn more.
|
||||
|
||||
#### Data types
|
||||
|
||||
| Type Name | Bytes | Range of Values |
|
||||
|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|
|
||||
| bool | 1 | `true` or `false` |
|
||||
| int8 | 1 | -128 to 127 |
|
||||
| uint8 | 1 | 0 to 255 |
|
||||
| int16 | 2 | -32,768 to 32,767 |
|
||||
| uint16 | 2 | 0 to 65,535 |
|
||||
| int32 | 4 | -2,147,483,648 to 2,147,483,647 |
|
||||
| uint32 | 4 | 0 to 4,294,967,295 |
|
||||
| float32 | 4 | 3.4E +/- 38 (7 digits) |
|
||||
| float64 | 8 | 1.7E +/- 308 (15 digits) |
|
||||
| string | varuint length prefix followed by bytes of each character | Any string |
|
||||
| varuint | 1 byte when 0 to 127<br /> 2 bytes when 128 to 16,383<br /> 3 bytes when 16,384 to 2,097,151<br /> 4 bytes when 2,097,152 to 268,435,455<br /> etc. | 0 to 2,147,483,647 |
|
||||
| varint | 1 byte when -64 to 63<br /> 2 bytes when -8,192 to 8,191<br /> 3 bytes when -1,048,576 to 1,048,575<br /> 4 bytes when -134,217,728 to 134,217,727<br /> etc. | -1,073,741,824 to 1,073,741,823 |
|
||||
| buffer | varuint length prefix followed by bytes of buffer | Any buffer |
|
||||
|
||||
### Packer
|
||||
|
||||
`Packer` is a higher-order function that may be used to pack all or part of the packet data
|
||||
regardless of the schema (e.g. JSON). `Packer` uses
|
||||
[`msgpack-lite`](https://github.com/kawanet/msgpack-lite) to serialize the packet data.
|
||||
|
||||
#### The entire packet
|
||||
|
||||
```javascript title="packages/example/src/packets/anything.js"
|
||||
export default (flecks) => {
|
||||
const {Packer, Packet} = flecks.fleck('@flecks/socket');
|
||||
const decorator = Packer();
|
||||
return class Anything extends decorator(Packet) {};
|
||||
};
|
||||
```
|
||||
|
||||
```javascript
|
||||
socket.send(['Anything', {foo: 'bar', arbitrary: 'data'}]);
|
||||
```
|
||||
|
||||
#### A specific key
|
||||
|
||||
```javascript title="packages/example/src/packets/anything-field.js"
|
||||
export default (flecks) => {
|
||||
const {Packer, Packet} = flecks.fleck('@flecks/socket');
|
||||
const decorator = Packer('document');
|
||||
return class AnythingField extends decorator(Packet) {
|
||||
|
||||
// Notice that we may still define other data fields here.
|
||||
static get data() {
|
||||
return {
|
||||
id: 'uint32',
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
```javascript
|
||||
socket.send([
|
||||
'AnythingField',
|
||||
{
|
||||
document: {whatever: 'you', want: 42},
|
||||
id: 1234567,
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
Packers ultimately pack to/from binary and are compiled down to the `buffer` data type in the
|
||||
schema.
|
||||
|
||||
### Bundles
|
||||
|
||||
A bundle is a bunch of packets that get packed into a single packet. Before you ask, **yes**, you can
|
||||
create `Bundle`s of `Bundle`s of packets. They're recursive!
|
||||
|
||||
```javascript
|
||||
const packets = [
|
||||
['Action', {type: 'SET', payload: 20}],
|
||||
['Bundle', [
|
||||
['Action', {type: 'ADD', payload: 23}],
|
||||
['Action', {type: 'ADD', payload: 26}],
|
||||
]],
|
||||
// ...
|
||||
];
|
||||
socket.send(['Bundle', packets]); // Nice.
|
||||
```
|
||||
|
||||
Bundles ultimately pack to/from binary and are compiled down to the `buffer` data type in the
|
||||
schema.
|
||||
|
||||
### Acceptors
|
||||
|
||||
Packets are subject to acceptors which validate and respond to packets. Your packets may implement
|
||||
acceptor methods:
|
||||
|
||||
#### `validate(packet, socket)`
|
||||
|
||||
##### `packet`
|
||||
|
||||
The packet being validated.
|
||||
|
||||
##### `socket`
|
||||
|
||||
The socket through which was sent the packet being validated.
|
||||
|
||||
`validate` acceptor methods may throw `ValidationError`s which `@flecks/socket` provides:
|
||||
|
||||
```javascript title="src/packets/some-packet.js"
|
||||
export default (flecks) => {
|
||||
const {Packet, ValidationError} = flecks.fleck('@flecks/socket');
|
||||
|
||||
return class SomePacket extends Packet {
|
||||
|
||||
static async validate(packet, {req}) {
|
||||
if (await req.checkSomething()) {
|
||||
throw new ValidationError({code: 404, reason: 'no something found'});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### `respond(packet, socket)`
|
||||
|
||||
##### `packet`
|
||||
|
||||
The packet being responded to.
|
||||
|
||||
##### `socket`
|
||||
|
||||
The socket through which was sent the packet being responded to.
|
||||
|
||||
:::note
|
||||
|
||||
The result from your `respond` acceptor method is serialized and transmitted and is ultimately the
|
||||
result of socket.send`:
|
||||
|
||||
```javascript
|
||||
const result = await socket.send(['Whatever']);
|
||||
// result is whatever was returned from the `respond` acceptor method.
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
#### Examples
|
||||
|
||||
- The `@flecks/redux` client implements the following `respond` acceptor method:
|
||||
|
||||
```javascript
|
||||
static async respond(packet) {
|
||||
flecks.redux.dispatch(packet.data);
|
||||
}
|
||||
```
|
||||
|
||||
Whenever the client receives an `Action` packet, the action will be dispatched by
|
||||
the redux store.
|
||||
|
||||
- `@flecks/user` implements a `validate` acceptor method for the `Logout` packet:
|
||||
|
||||
```javascript
|
||||
static validate(packet, {req}) {
|
||||
if (!req.user) {
|
||||
throw new ValidationError({code: 400, reason: 'anonymous'});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `req.user` doesn't exist, the packet fails validation.
|
||||
|
||||
## Intercom
|
||||
|
||||
flecks provides **intercom** as a way for your socket server nodes to communicate amongst
|
||||
themselves. Intercom is provided on a server socket at `socket.req.intercom` and has two
|
||||
parameters:
|
||||
|
||||
### `type`
|
||||
|
||||
The type of intercom call to make (e.g. `@flecks/user.users`).
|
||||
|
||||
### `data`
|
||||
|
||||
Arbitrary serializable data to send along with the intercom request.
|
||||
|
||||
:::tip[Fun fact]
|
||||
|
||||
Intercom can also be called from an implementation of `@flecks/web/server.request.socket` through
|
||||
`req.intercom`.
|
||||
|
||||
:::
|
||||
|
||||
### Motivation
|
||||
|
||||
Suppose that we are running many game simulation nodes which each have many clients connected. The
|
||||
nodes are processing a large number of positions in real time. Clients connected to any
|
||||
node must be able to request a position even if it is simulated on another node. A client connected
|
||||
to a node may request a position `P`. A node may efficiently check if it simulates `P`.
|
||||
|
||||
If the node that dispatches the request simulates the position, the response can be instant.
|
||||
Otherwise, the node must use **intercom** to ask the other nodes whether they simulate `P`:
|
||||
|
||||
```javascript
|
||||
const responses = await socket.req.intercom('@simulation/position.request', P);
|
||||
```
|
||||
|
||||
This asks the other nodes if they simulate `P`. Nodes will respond either with `undefined` or `P`
|
||||
through their implementations of `@flecks/socket.intercom`:
|
||||
|
||||
```javascript
|
||||
export const hooks = {
|
||||
'@flecks/socket.intercom': async (P, {simulation}) => {
|
||||
if (simulation.has(P)) {
|
||||
return simulation.positionOf(P);
|
||||
}
|
||||
// return undefined is implied...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
Intercom **will** invoke the `@flecks/socket.intercom` implementation of the requesting node.
|
||||
|
||||
:::
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Client
|
||||
participant Node-1
|
||||
participant Node-2
|
||||
participant Node-3
|
||||
participant Node-N
|
||||
Note right of Client: Client makes a request for position(P)
|
||||
Client->>Node-1: REQUEST: position(P)
|
||||
alt has position(P)
|
||||
Note left of Node-1: Has position: no intercom necessary.
|
||||
Node-1->>Client: RESPONSE: {x, y, z}
|
||||
else doesn't have position(P)
|
||||
Note right of Node-1: Doesn't have position: intercom necessary.
|
||||
Node-1->>Intercom: INTERCOM: @simulation/position.request(P)
|
||||
par
|
||||
Intercom-->>Node-1: INTERCOM: @simulation/position.request(P)
|
||||
Node-1-xIntercom: RESPONSE: undefined
|
||||
Note right of Node-1: Doesn't have position: undefined.
|
||||
and
|
||||
Intercom-->>Node-2: INTERCOM: @simulation/position.request(P)
|
||||
Node-2-xIntercom: RESPONSE: {x, y, z}
|
||||
Note right of Node-2: Has position: {x, y, z}.
|
||||
and
|
||||
Intercom-->>Node-N: INTERCOM: @simulation/position.request(P)
|
||||
Node-N-xIntercom: RESPONSE: undefined
|
||||
Note right of Node-N: Doesn't have position: undefined.
|
||||
end
|
||||
Note left of Intercom: Return all results as an array.
|
||||
Intercom->>Node-1: RESPONSE: [undefined, {x, y, z}, undefined]
|
||||
Note left of Node-1: Filter out and return the position from the results.
|
||||
Node-1->>Client: RESPONSE: {x, y, z}
|
||||
end
|
||||
```
|
||||
|
||||
## Default packets
|
||||
|
||||
`@flecks/socket` provides some packets by default:
|
||||
|
||||
### `Refresh`
|
||||
|
||||
Sent to a client, refreshes the page.
|
||||
|
||||
```javascript
|
||||
socket.send(['Refresh']);
|
||||
```
|
||||
|
||||
### `Redirect`
|
||||
|
||||
Send a string which will be assigned to `window.location.href`.
|
||||
|
||||
```javascript
|
||||
socket.send(['Redirect', '/']);
|
||||
```
|
||||
|
||||
See [the generated hooks page](./flecks/@flecks/dox/hooks#fleckssocketpackets) for an exhaustive list of packets.
|
||||
|
|
Loading…
Reference in New Issue
Block a user