flecks/website/docs/sockets.mdx
2024-01-06 20:31:12 -06:00

354 lines
12 KiB
Plaintext

---
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[Move the payload!]
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[Full circle]
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[Putting the web in websockets]
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[Is that really what I sound like?]
Intercom **will** invoke the `@flecks/socket.intercom` implementation of the requesting node, not
only the others.
:::
```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.