Skip to content

Commit

Permalink
mqtt initial docs (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienGllmt authored Jul 29, 2024
1 parent e54a790 commit ad00626
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/home/300-react-to-events/1-scheduled-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_position: 1

# Scheduling Events and Timers

Games heavily rely on passive time to trigger events, such as limits on the length of a match or the duration of status effects. Paima supports these through a generic system called `scheduledData` that keeps track of which inputs (that conform to your app's grammar) to trigger at which block height (used instead of timestamps).
Games heavily rely on passive time to trigger events (often based on *ticks*), such as limits on the length of a match or the duration of status effects. Paima supports these through a generic system called `scheduledData` that keeps track of which inputs (that conform to your app's grammar) to trigger at which block height (used instead of timestamps).

Paima will fetch, execute and commit the result of any scheduled data for a block BEFORE it considers any regular input inside the block.

Expand Down
21 changes: 21 additions & 0 deletions docs/home/350-game-node-api/100-events/1-introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Introduction

When writing a application, being able to easily know usage patterns is crucial to:
1. As the core developers, to know what is working and what isn't
2. As an external developer, to be able to build interesting projects on top of the core protocol

Although there are cases where having [relational databases](https://en.wikipedia.org/wiki/Relational_database) can provide a lot of speed, clarity and usability benefits, in a lot of cases [non-relational databases](https://en.wikipedia.org/wiki/NoSQL) are significantly easier to write and much easier to consume externally. This is especially true in cases where use-generated content plays a key role in the protocol, as you often cannot know which relational structures best fit user behavior ahead of time. You can see this in practice for example with blockchains, where often there are relational database options for parts that seldom change (the core protocol itself), whereas user-generated content (ex: dApps) are often indexed using general logging systems (ex: [event logs](https://docs.alchemy.com/docs/deep-dive-into-eth_getlogs) for Solidity).

Similarly, for Paima Engine, we provide [database management](../../500-database-management/100-introduction.md) of rollup state out of the box, but also provide a simpler logging system for cases where it makes sense.

Notably, for Paima's event system, we had the following desirable properties:
1. **Events should be customizable**
1. Events can be defined on a per-app basis, be emitted wherever needed (ex: an event if a player completes a quest)
1. Events can self-define which fields are [indexable](https://en.wikipedia.org/wiki/Database_index) to fine-tune performance
2. **Events should be easily parsable**
1. Events should support static typing using [json-schema](https://json-schema.org/learn/getting-started-step-by-step)
1. Events should come with a code generation and documentation generation to make it easy for external developers to understand
3. **Events should be easy to access**
1. Historical events are kept permanently by default
1. (for historical data) Events should be accessible via a [REST](https://en.wikipedia.org/wiki/REST) interface
1. (for realtime data) Events should be accessible via a [pub-sub system](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)
68 changes: 68 additions & 0 deletions docs/home/350-game-node-api/100-events/100-general-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# General Interface

Events are defined by two components:
1. A `name`, which must be unique in your app
1. A set of `fields` which defines the event content. Fields are made up of
1. A `name` for a *positional argument*
2. A `type` defined with [typebox](https://github.com/sinclairzx81/typebox) to ensure JSON compatibility
3. A `indexed` boolean for whether or not events will be [indexable](https://en.wikipedia.org/wiki/Database_index) on this field

Here is an example of an event that tracks completing quests in a game. In this example, we create an `index` on the `playerId` so a client could get realtime updates whenever a user has completing a quest.

```typescript
import { Type } from '@sinclair/typebox';
import { genEvent } from './types.js';

const QuestCompletionEvent = genEvent({
name: 'QuestCompletion',
fields: [
{
name: 'playerId',
type: Type.Integer(),
indexed: true,
},
{
name: 'questId',
type: Type.Integer(),
},
],
} as const);
```

*TODO*: the API to register these events with Paima Engine itself is still under development

# Listening to events

You can listen to these events easily from Javascript just by importing the event definition

A few things to note:
1. You can `filter` events based on the `indexed` fields. Filtering is optional, and you can filter by any set (or subset) of `indexed` fields of the event
2. You can register multiple subscribers to the *same* event. Paima Engine will handle [multiplexing](https://en.wikipedia.org/wiki/Multiplexing) under the hood so that subscriptions with the same `event+filter` combination do not cause duplicate network requests
3. You can call the `unsubscribe` response at any time to lower the number of open connections you have

```ts
import { PaimaEventManager } from '@paima/sdk/events';

const unsubscribe = await PaimaEventManager.Instance.subscribe(
{
topic: QuestCompletionEvent,
filter: { playerId: undefined }, // all players
},
event => {
console.log(`Quest ${event.questId} cleared by ${event.playerId}`);
}
);

// later
await unsubscribe();
```

# Posting new events

You can publish messages from your game's state machine at any time. You need to provide *all* fields (both those indexed and those that aren't). Paima Engine, under the hood, will take care of only sending these events to the clients that need them.

```ts
import { PaimaEventManager } from '@paima/sdk/events';

await PaimaEventListener.Instance.sendMessage(QuestCompletionEvent, { questId: 5, playerId: 10 });
```
12 changes: 12 additions & 0 deletions docs/home/350-game-node-api/100-events/110-builtin-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Built-in events

Paima Engine comes with built-in events:
1. `BatcherHash` for whenever a [batcher](../../200-read-write-L2-state/400-batched-mode.md) has an update about the progress of a user's transtion
2. `RollupBlock` for whenever a new block is created in the rollup

These built-in events can be imported using
```ts
import { BuiltinEvents } from '@paima/sdk/events';
```

<!-- TODO: add more docs on these once we have the docgen ready -->
69 changes: 69 additions & 0 deletions docs/home/350-game-node-api/100-events/200-low-level-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Low-level API

Under the hood, Paima Engine uses [MQTT](https://mqtt.org/) for its event system. MQTT was chosen because:
1. It allows subscribing to realtime events based on a subset of filters ([docs](https://www.emqx.com/en/blog/advanced-features-of-mqtt-topics))
1. It has [websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) support for event streaming
1. It allows QoS (*"Quality of Service"*) level including guaranteed message delivery ([docs](https://www.emqx.com/en/blog/introduction-to-mqtt-qos) - Paima sets a `qos` of 2 by default)
1. It can scale with [brokers](https://www.emqx.com/en/blog/mqtt-5-introduction-to-publish-subscribe-model) optionally, but has an [in-memory client](https://github.com/moscajs/aedes) for use-cases that don't need the scale (which Paima uses)

More concretely, when you define an event using Paima, `indexed` fields define the *topic* of the MQTT message and the non-indexed fields define the *content*

## Example

For example, for the given event that defines quest completion in a game,

```ts
const QuestCompletionEvent = genEvent({
name: 'QuestCompletion',
fields: [
{
name: 'playerId',
type: Type.Integer(),
indexed: true,
},
{
name: 'questId',
type: Type.Integer(),
},
],
} as const);
```

- The MQTT topic generated is `game/playerId/{playerId}` (ex: `game/playerId/1`)
- The content of the MQTT messages is `{ questId: number }`

Note that all events starts with a prefix depending on its origin (`TopicPrefix`):
- `app` for events defined by the user
- `batcher` for events coming from the [batcher](../../200-read-write-L2-state/400-batched-mode.md)
- `node` for events that come from the Paima Engine node

## Manually writing MQTT

If you prefer to have more fine-grained controlled over the MQTT syntax (notably the way *topics* are generated), you can write events using the following direct form (internally, the event syntax gets converted down to this form)

<!-- TODO: better syntax for this that doesn't expose things like TopicPrefix or PaimaEventBrokerNames -->

```ts
export const Events = {
QuestCompletion: {
path: ["foo", "bar", { name: 'playerId', type: Type.Integer() }],
type: Type.Object({
questId: Type.Integer(),
}),
},
} as const satisfies Record<string, Omit<EventPathAndDef, 'broker'>>;
```

This example generates the topic `app/foo/bar/{playerId}`

## Debugging

To debug MQTT calls to see if they work correctly, you can use the following tool

```bash
npm i -g mqtt-client-cli; # only need to install once

mqtt-client-cli ws://localhost:8883;
# example subscription
sub node/block/#
```
3 changes: 3 additions & 0 deletions docs/home/350-game-node-api/100-events/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label": "Events"
}

0 comments on commit ad00626

Please sign in to comment.