Skip to content

Commit

Permalink
docs: details on custom event ids and event versions
Browse files Browse the repository at this point in the history
  • Loading branch information
kengoldfarb committed Jun 19, 2019
1 parent 5fb8fae commit 603a2e9
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 62 deletions.
154 changes: 92 additions & 62 deletions spruce-skill/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,18 @@ The following diagram follows the `did-enter` event as it flows through the syst
<img src="images/did-enter.gif?raw=true" />
</p>

## Event object
# Event Versions

The `event` object is really a [`user`](user.md) object, with one exception; `event.User` and associated fields are optional. Whether or not an `event.User` exists in the `event` is up to the Skill that emits it. For core and 99% of skills, you can expect `event.User` and it's associated fields to exist. Each skill _should_ document the events they `emit`, so you won't be guessing. In fact, at the time of this writing, there is not a single case where a `event.User` is not provided.
As the platform evolves to support new features with breaking changes to the event system, the `EVENT_VERSION` environment variable allows controlling which version of events your skill can receive and process.

```js
{
Location: Location, // required
User: User, // optional
createdAt: Date, // date the guest joined the location (optional)
updatedAt: Date, // date the guest changed their subscription to the location (optional)
role: String, // owner|teammate|guest (optional)
status: String, // offline|online (optional)
visits: Number, // how many visits to this location (optional)
lastRecordedVisit: Date, // when they last visited the shop (optional)
payload: Object // data the skill or core is passing on (optional)
}
```
Current Versions:

## Core Events
[Event Version 1](./eventsV1.md) - The current default version. Skill will use version 1 unless otherwise specified.

<!-- TODO: Add links for heartwood and skill views -->
[Event Version 2](./eventsV2.md) - The new event version, used in conjunction with Heartwood and Skill Views

These events are built in. They all come with `event.User`.
## Core Events

- `did-signup` - When a guest signs up at a location. Check ctx.status === 'online' to know if they are on the wifi
- `did-enter` - When a guest returns and their phone hits the wifi
Expand Down Expand Up @@ -64,14 +55,9 @@ module.exports = async (ctx, next) => {
}
```

## Listening to custom events
## Event naming

A custom `event` is broken into 2 segments, the `slug` of the skill emitting it and the `event-name`. For example, the `Vip Alert` skill will emit `vip-alerts:will-send` just before an alert is sent to the team. You can hook into this event and cancel it, modify the messages sent, or send your own alerts. If you replace the `:` with a `/`, you'll have your file path.

- `vip-alerts:will-send` -> `server/events/vip-alerts/will-send.js`
- `scratch-win:will-manually-send` -> `server/events/scratch-win/will-manually-send.js`

## CRUD Events
### Naming events that your skill listens to

For events you listen to that perform an action in your skill, follow a <verb>-<noun> naming convention. For example, I might emit `booking:create-appointment` or `booking:get-location-services`

Expand All @@ -85,6 +71,35 @@ For events you listen to that perform an action in your skill, follow a <verb>-<

- Delete (DELETE): `delete`

### Naming events that are triggered by actions in your skill

We follow a `will`/`did` convention for our events. When creating your own event, start it with `will-` or `did-`.

`Will` events happen before an operation and should always honor `preventDefault`.

`Did` events happen after any operation.

```js
// emit your custom event
const responses = await this.sb.emit(
guest.Location.id,
'myskill:will-do-something',
{
foo: 'bar'
}
)

// did anyone prevent default?
const preventDefault = responses.reduce((preventDefault, response) => {
return preventDefault || !!response.payload.preventDefault
}, false)

// bail
if (preventDefault) {
return false
}
```

## Emitting custom events

Skills communicate with each other using the `event` system. Custom events are in the form of: <tense>-<verb>-<noun>, `will-send-vip-alert`.
Expand All @@ -107,15 +122,39 @@ const responses = await ctx.sb.emit(ctx.auth.Location.id, 'scratch-and-win:will-
console.log(responses) // [EventResponse, EventResponse]
```

## Event Contracts
### `eventId`s and `retryId`s

Every event comes with an `eventId` which is a UUID and is automatically generated. If this is an event retry, it will also have a unique `retryId`. Logging the `eventId` can aid with debugging.

The `eventId` can be accessed on an incoming event at `ctx.event.eventId`

#### Custom event ids

In order to receive events you'll need to subscribe to them. Likewise, in order to emit events you should publish them along with information about required parameters. You'll set these up in `config/default.js`
Sometimes it might be useful to control the `eventId` that is used for a particular emitted event. You can pass a custom eventId:

```js
const eventId = uuid.v4() // The eventId MUST BE A UUID
const responses = await ctx.sb.emit(ctx.auth.Location.id, 'scratch-and-win:will-manually-send', {
userId: ctx.user.User.id, // the id of the guest (ctx.user set in middleware)
message: ctx.utilities.lang('manualSendMessage', {
to: ctx.user.User.id,
from: ctx.auth.User.id
}),
teammateId: ctx.auth.User.id,
sendToSelf: config.DEV_MODE // this event will emit back to you (for testing)
}, {}, eventId)
```
eventContract: {
events: {
///////// CUSTOM EVENTS /////////
'booking:get-service': {

## Listening to events

### Step 1: Event Contracts

In order to receive events you'll need to subscribe to them. You'll set these up in `config/default.js`

<!--
TODO: Add docs on schema when implemented...for example:
'booking:get-service': {
description: 'Gets a single service',
publish: true,
subscribe: false,
Expand Down Expand Up @@ -145,7 +184,12 @@ eventContract: {
}
}
},
///////// CORE EVENTS /////////
-->

```
eventContract: {
events: {
'did-message': {
subscribe: true
},
Expand All @@ -161,11 +205,26 @@ eventContract: {
description: 'When the skill is installed to a location',
subscribe: true
},
'booking:did-cancel-appointment': {
description: 'Booking appointment was cancelled',
subscribe: true,
}
}
}
```

### Step 2: Create the event handler file

For core events, you'll create files such as:

- `did-enter` -> `server/events/did-enter.js`

A custom `event` is broken into 2 segments, the `slug` of the skill emitting it and the `event-name`. For example, the `Vip Alert` skill will emit `vip-alerts:will-send` just before an alert is sent to the team. You can hook into this event and cancel it, modify the messages sent, or send your own alerts. If you replace the `:` with a `/`, you'll have your file path.

- `vip-alerts:will-send` -> `server/events/vip-alerts/will-send.js`
-
- `scratch-win:will-manually-send` -> `server/events/scratch-win/will-manually-send.js`

## EventResponse object

```js
Expand Down Expand Up @@ -202,40 +261,11 @@ module.exports = async (ctx, next) => {
}
```

## Naming events that are triggered by actions in your skill

We follow a `will`/`did` convention for our events. When creating your own event, start it with `will-` or `did-`.

`Will` events happen before an operation and should always honor `preventDefault`.

`Did` events happen after any operation.

```js
// emit your custom event
const responses = await this.sb.emit(
guest.Location.id,
'myskill:will-do-something',
{
foo: 'bar'
}
)

// did anyone prevent default?
const preventDefault = responses.reduce((preventDefault, response) => {
return preventDefault || !!response.payload.preventDefault
}, false)

// bail
if (preventDefault) {
return false
}
```

## Gotchya's

- Event listeners need to respond in 5 seconds or they will be ignored. That means you may need to respond to Sprucebot right away and run your logic after.
- Custom events will not `emit` back to your skill unless you set `sendToSelf=true`. This makes testing way easier, but should def be off in production (why we tie it to `DEV_MODE=true`).
- Your skill's `slug` can't be arbitrary. It is assigned to you by Spruce Labs when you begin creating your skill.
- Your skill's `slug` can't be arbitrary. It is assigned to you by Spruce Labs when you begin creating your skill and **can not be changed**.

## Examples

Expand Down
19 changes: 19 additions & 0 deletions spruce-skill/eventsV1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Event Version 1

## Event object

The `event` object is really a [`user`](user.md) object, with one exception; `event.User` and associated fields are optional. Whether or not an `event.User` exists in the `event` is up to the Skill that emits it. For core and 99% of skills, you can expect `event.User` and it's associated fields to exist. Each skill _should_ document the events they `emit`, so you won't be guessing. In fact, at the time of this writing, there is not a single case where a `event.User` is not provided.

```js
{
Location: Location, // required
User: User, // optional
createdAt: Date, // date the guest joined the location (optional)
updatedAt: Date, // date the guest changed their subscription to the location (optional)
role: String, // owner|teammate|guest (optional)
status: String, // offline|online (optional)
visits: Number, // how many visits to this location (optional)
lastRecordedVisit: Date, // when they last visited the shop (optional)
payload: Object // data the skill or core is passing on (optional)
}
```
39 changes: 39 additions & 0 deletions spruce-skill/eventsV2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Event Version 2

The latest event version which supports Skill Views.

## Event Object

The incoming event is automatically processed and _might_ contain any (or none) of:

* `userId` - The id of the User associated with this event

* `locationId` - The id of the Location associated with this event

* `organizationId` - The id of the Organization associated with this event

If these ids are detected, a call is made to the Core API to get the data which are then attached to `ctx.auth`

The data that is fetched and attached to `ctx.auth` is based on [your skill's config/auth.js file](https://github.com/sprucelabsai/workspace.sprucebot-skills-kit/blob/dev/packages/spruce-skill/config/auth.js).

In practice your incoming event will look like:

```js
const { event, auth } = ctx
// "payload" is the data emitted with the event
// "eventId" is a unique id associated with this event
// "retryId" is only defined if this event is being retried
// "name" is the event name: 'did-enter' for example
const { payload, eventId, retryId, name } = event

let User
let Location
let Organization
if (auth) { // auth may not be defined
User = auth.User // might not be defined
Location = auth.Location // might not be defined
Organization = auth.Organization // might not be defined
}

// Handle the event...
```

0 comments on commit 603a2e9

Please sign in to comment.