-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e54a790
commit ad00626
Showing
6 changed files
with
174 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
68
docs/home/350-game-node-api/100-events/100-general-interface.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
12
docs/home/350-game-node-api/100-events/110-builtin-events.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
69
docs/home/350-game-node-api/100-events/200-low-level-api.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/# | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"label": "Events" | ||
} |