Skip to main content

Events

This service provides a pub/sub system for event handling.

Examples

Registering an Event

An event can be registered by calling registerEvent. Event registering can only be done from a Typescript file, in the entries directory, following the naming convention entries/APPNAME/entry.ts. In order to keep our event system maintainable, all events must be codified in a Typescript type before they can be registered. Adding the types is a straightforward process and the example below includes how we can do this:

import { registerEvent } from '@rhapsody/events'

// ↓↓↓ Here is where we define the types for the events we're about to register.
// Each event should be listed with the key being the event name and the type
// being the type for any data passed along with the event. If no data will be
// included, `void` should be used as the type. Notice also how `PubSubEventsMap`
// does not need to be imported. We can append members to it directly using the
// syntax below. See the documentation for `PubSubEventsMap` for more information.
declare module '@rhapsody/events/types' {
interface PubSubEventsMap {
'event.app.workflow.sidebar.isVisible': boolean
'event.app.workflow.sidebar.show': void
}
}

registerEvent('event.app.workflow.sidebar.isVisible')
registerEvent('event.app.workflow.sidebar.show')

Regarding event naming, all events are required to adhere to the naming convention event.(global|shared|app).<NAMESPACE>.<NAMESPACE>.<OPTIONAL_NAMESPACE>. Below are a few annotated examples:

  • event.global.user.isLoaded: A global event namespaced to when the user has been loaded. In this case, the event will be published as a result of an action taking place.
  • event.app.dialer.pane.show: An app-specific event namespaced to the dialer app to show the dialer pane. In this case, the event will be published to trigger an action.
More Information on Typing in the Events Service

In order to keep the events service maintainable, we require all events to be typed. This involved both 1. providing the event name and 2. providing the type of the payload passed along with the event. We accomplish this using some fancy type footwork as well as by utilizing a Typescript feature called declaration merging.

The primary interface the events system utilizes is PubSubEventsMap. This interface starts out as an empty interface and is augmented by other files in the project to build up the full list of event names. We gather the full list of event names by using keyof PubSubEventsMap. This is what gives us autocomplete for the first parameter in all the exported functions.

Building up this type uses declaration merging. With declaration merging, we can append members (key/type pairs) to an interface that's already been declared elsewhere. In the example above, the line declare module '@rhapsody/events/types' tells Typescript we're going to be extending the types.ts file in the @rhapsody/events package. From there, adding to the interface looks exactly the same as defining a regular interface. Once caveat is that one cannot overwrite an existing member unless it is over the same type. This isn't an issue for us with PubSubEventsMap because each event name must be globally unique.

Publishing an Event

Events can be published by calling publishEvent. An event payload can optionally be included and should match the type defined when registering the event. If publishing from a Typescript file, the type will be enforced by the compiler.

import { publishEvent } from '@rhapsody/events'

publishEvent('event.app.workflow.sidebar.isVisible', true)
publishEvent('event.app.workflow.sidebar.show')

Subscribing to an Event

We can subscribe to events using subscribeToEvent. subscribeToEvent requires the event name to subscribe to, a callback that will fire every time the event occurs, and metadata about the subscriber. A function is returned that can be used to unsubscribe.

import { subscribeToEvent } from '@rhapsody/events'

const unsubscribe = subscribeToEvent(
'event.app.workflow.sidebar.isVisible',
(payload) => {
console.log('Is the sidebar visible?', payload)
},
{ name: 'sidebar visibility logger' }
)

// Some time in the future
unsubscribe()

Subscribing to an Event in React

When working in React, we can use useEventSubscription to subscribe to events in React components and automatically unsubscribe on unmount. The arguments for useEventSubscription are the same as those for subscribeToEvent. Optionally we can pass the dependencies array to effect if the values in the list change

function SidebarVisibility() {
const [isSidebarVisible, setIsSidebarVisible] = useState(false)
useEventSubscription(
'event.app.workflow.sidebar.isVisible',
(isVisible: boolean) => {
setIsSidebarVisible(isVisible)
},
{ name: 'SidebarVisibility component' },
[dependency1, dependency2]
)

return <div>Is the sidebar visible? {isSidebarVisible ? 'Yes' : 'No'}</div>
}

Testing

This package has a mock in place for use with jest.mock. See the example below to on how to test publishing and subscribing:

import React from 'react'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { publishEvent, subscribeToEvent } from '@rhapsody/events'

// BEGIN CODE REQUIRED TO MOCK EVENT SERVICE
jest.mock('@rhapsody/events')

afterEach(() => {
jest.clearAllMocks()
})
// END CODE REQUIRED TO MOCK EVENT SERVICE

function fnToTestPublish() {
publishEvent('event.app.workflow.sidebar.show')
}

let isVisible = false
function fnToTestSubscribe() {
subscribeToEvent(
'event.app.workflow.sidebar.isVisible',
(payload) => {
isVisible = payload
},
{ name: 'test' }
)
}

describe('example', () => {
it('publishes the "event.app.workflow.sidebar.show" event', () => {
fnToTestPublish()
expect(publishEvent).toHaveBeenCalledWith('event.app.workflow.sidebar.show')
})

it('subscribes to the "event.app.workflow.sidebar.isVisible" event', async () => {
fnToTestSubscribe()

expect(isVisible).toBe(false)
publishEvent('event.app.workflow.sidebar.isVisible', true)
await waitFor(() => expect(isVisible).toBe(true))
publishEvent('event.app.workflow.sidebar.isVisible', false)
await waitFor(() => expect(isVisible).toBe(false))
})
})

Dev-time Event Logger

In development builds, an event logger is included with the events service. The function to enable it is attached to window and the logger can be toggled by calling window.__toggleSalesloftEventLogger() in the browser console. The enabled state of the logger is stored in SessionStorage and will persist across page reloads until the tab/window is closed. The logger prints events as expanded groups by default, this can be changed by providing a config window.__toggleSalesloftEventLogger({ expand: false })

In production, the logger is stripped from the bundle.