Skip to content

Commit

Permalink
clean up docs, prepend clientName to eventClass if not included
Browse files Browse the repository at this point in the history
  • Loading branch information
mackenzie-grimes-noaa committed Nov 7, 2024
1 parent 12fde6f commit 97ce2db
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 37 deletions.
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
This is a React Javascript library for two or more React apps to connect to MUPPET data channels (WebRTC/websockets under the hood), then use simple, React-like interfaces to forward each other serialized UI events from their app (like user clicks, selections, or input) via the [MUPPETs protocol](https://docs.google.com/document/d/1TSvRtfzQGdclHGys9e0dLXKNnvWAmRnizH-biQW066o/view?usp=sharing).

## Table of Contents

- [IDSSe MUPPET Library](#idsse-muppet-library)
- [Table of Contents](#table-of-contents)
- [Usage](#usage)
Expand Down Expand Up @@ -49,7 +50,72 @@ npm install @noaa-gsl/idsse-muppet
Now you can use the MUPPET library to create new connections to a MUPPET channel and send/receive events over it in your React application.

### Developer Guide
For React apps, see the [Developer Guide - React](docs/react.md).

To use MUPPET channels in your React app, first need to wrap your top-level React component in a `MuppetProvider`:

```javascript
// main.jsx
import ReactDOM from "react-dom/client";
import { MuppetProvider } from "@noaa-gsl/idsse-muppet";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
<MuppetProvider
clientName="MY_APP"
serverUrl="http://example.com" // URL of WebRTC signaling server
serverPath="/"
channelNames={["my-channel"]}
>
<App>
</MuppetProvider>
);
```

- `channelNames`: 1 or more WebRTC rooms on the WebRTC server that you wish to connect to. This will need to line up with the room(s) used by the app with which you want to send/receive messages.
- `clientName`: how your app identifies itself to other apps using MUPPET. This will be prepended to the `eventClass` of every event you send. For example, other apps may subscribe to events from you with eventClass `MY_APP.SOME_BUTTON_CLICKED`, or if they want to send an RPC type message to your app specifically, they would use this clientName string to address the message.

With that Provider in place, now any components in your React component tree have access to shared, persistent `MuppetChannel` instances. You can fetch this channel and use it directly in your React component:

```javascript
// MyComponent.jsx
import { useState } from 'react';
import { useMuppetChannel, useMuppetCallback } from '@noaa-gsl/idsse-muppet';

function MyComponent() {
// track user's favorite color from other app
const [currentColor, setCurrentColor] = useState('');

const channel = useMuppetChannel('my-channel');

useMuppetCallback(
'my-channel',
(channel, evt) => {
console.log('Received new color selection from OTHER_APP:', evt);
setCurrentColor(event.color);
},
['OTHER_APP.COLOR_SELECTED'],
);

const onButtonClick = () => {
channel?.sendEvent({
eventClass: 'BUTTON_CLICKED',
event: { value: 123 },
});
};

return (
<div>
<button onClick={onButtonClick}>Hello world</button>
<p>Favorite color:</p>
<p>{currentColor}</p>
</div>
);
}

export default MyComponent;
```
For more details, see the [Developer Guide - React](docs/react.md).
Although it's not recommended because it can be much harder to manage app state, if you want to use the underlying JS without the niceties of React Context or custom Hooks, see [Developer Guide - Vanilla JS](docs/vanilla-js.md)
Expand Down
112 changes: 78 additions & 34 deletions docs/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

- [Developer Guide - React](#developer-guide---react)
- [Table of Contents](#table-of-contents)
- [Initialize a new MUPPET channel](#initialize-a-new-muppet-channel)
- [Broadcast an event](#broadcast-an-event)
- [Receive events](#receive-events)
- [Get all channels](#get-all-channels)
- [Send an RPC request](#send-an-rpc-request)
- [`MuppetProvider`](#muppetprovider)
- [Hooks](#hooks)
- [`useMuppetChannel`](#usemuppetchannel)
- [`useMuppetChannels`](#usemuppetchannels)
- [`useMuppetCallback`](#usemuppetcallback)
- [`MuppetChannel`](#muppetchannel)
- [`sendEvent()`](#sendevent)
- [`sendRequest()`](#sendrequest)
- [`sendRawEvent()`](#sendrawevent)

## Initialize a new MUPPET channel
## `MuppetProvider`

First, in your `main.jsx` file, add a `<MuppetProvider>` component wrapping your main React app component. Pass the Provider a string array `channelNames`, which is a list of WebRTC rooms on the server you wish to connect to.
In the `main.jsx` file of your React app, add a `<MuppetProvider>` component wrapping your top-level React app component (typically called "App"). Pass the Provider a string array `channelNames`, which is a list of WebRTC rooms on the server you wish to connect to.

```javascript
// main.jsx
Expand All @@ -34,7 +38,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(

These channel name(s) and clientName should be coordinated beforehand with the other web apps you wish to communicate via MUPPET; your app and the other app must connect to the same channel on the server for the apps to "find each other" (negotiate a peer-to-peer websocket) and start sending messages. The `clientName` is important because the other app may wish to subscribe to events published by you (which will start with your `clientName`). You can pass any number of `channelNames` to the Provider, which will attempt to create individual connections to each channel listed.

This Provider now stores an app-wide [React Context](https://react.dev/learn/passing-data-deeply-with-context) for you, so any components in your component tree can reuse the same persistant `MuppetChannel` by calling the `useMuppetChannel()` React hook with one of the channel names. See the [Broadcast an event](#broadcast-an-event) section for more details.
This Provider stores an app-wide [React Context](https://react.dev/learn/passing-data-deeply-with-context) for you, so any components in your component tree can reuse the same persistant `MuppetChannel` by calling the [useMuppetChannel()](#usemuppetchannel) React hook with one of the channel names.

> For reliability, it's recommended that this room is unique to your session/browser ("my-room-abc123", for example, instead of just "my-room"). Consider establishing with the app with which you're integrating some shared nonce or algorithm to generate a new room for each new user session, so user A using your app will not have problems with user B's click actions taking effect in their session.
>
Expand All @@ -58,37 +62,51 @@ if (channel.isOpen()) {
>
> As soon as both apps have connected to the same MuppetChannel, all of these pending events will be "replayed"--sent over the channel--so the receiver can receive them.
## Broadcast an event
## Hooks

To broadcast a MUPPET event over your new MuppetChannel, simply pass your MUPPET eventClass and event to `MuppetChannel.sendEvent()`.
### `useMuppetChannel`

This can (and generally should) be invoked right in the Javascript component where the user took action. For example, the `onClick` callback of some button, or an `onSelect` of an HTML select element:
Assuming you created a React `MuppetProvider` from above and passed `channelNames` to it, the Provider stores and exposes to any component in your React app tree the `MuppetChannel` instances for those channel names.

```javascript
// MyComponent.jsx
import { useEffect } from 'react';
import { useMuppetChannel } from '@noaa-gsl/idsse-muppet';

function MyComponent() {
const channel = useMuppetChannel('my-channel');

const onButtonClick = () => {
channel?.sendEvent({
eventClass: 'MY_APP.SOME_EVENT',
event: { value: 123 },
});
};

return <button onClick={onButtonClick}>Hello world</button>;
useEffect(() => {
console.log('Got MUPPET channel, current status:', channel?.state);
}, [channel]);
}

export default MyComponent;
```
Note the component must know the "channel name" string given to `MuppetProvider`, so it can reference the `MuppetChannel` object it wants to use, as multiple channels can be stored in MuppetProvider Context. In the example above, the exact channel name is `my-channel`, but if a non-existent channel was requested by the component, the `useMuppetChannel()` hook would return `undefined`.
## Receive events
### `useMuppetChannels`
You can also get the mapping of all channel names to their `MuppetChannel` instances by calling `useMuppetChannels()`. Remember, these channel names will match exactly the strings passed to `MuppetProvider`, and may be null if they haven't attempted to connect yet.
Assuming you created a React `MuppetProvider` from above and passed `channelNames` to it, the Provider stores and exposes to any component in your React app tree the `MuppetChannel` instances for those channel names. Use the Hook `useMuppetCallback` similar to how you might wire up a callback function to a given React state change using [useEffect()](https://react.dev/reference/react/hooks#effect-hooks).
For example, if you passed `channelNames={["some-channel", "another-channel"]}` to the overall `MuppetProvider`:
```javascript
const channelMap = useMuppetChannels();
const { 'some-channel': someChannel, 'another-channel': anotherChannel } = channelMap;

console.log('Is some-channel open?', someChannel.isOpen());

anotherChannel?.sendEvent({
eventClass: 'MY_APP.COLOR_SELECTED',
event: { color: 'blue' },
});
```
### `useMuppetCallback`
Assuming you created a React `MuppetProvider` from above and passed `channelNames` to it, the Provider stores and exposes to any component in your React app tree the `MuppetChannel` instances for those channel names.
Use the Hook `useMuppetCallback` similar to how you might wire up a callback function to a given React state change using [useEffect()](https://react.dev/reference/react/hooks#effect-hooks).
```javascript
// MyComponent.jsx
Expand Down Expand Up @@ -117,25 +135,35 @@ Since WebRTC itself has no rules or conventions on how data sent over channels s
> Note: you will need to coordinate with the app you're integrating with to determine the eventClass constants that it plans to send you. According to MUPPET conventions, it should be declared in JSON Schema the body of a special `SCHEMAS` eventClass message that the other app sends you immediately after you both connect.
### Get all channels
## `MuppetChannel`
You can also get the mapping of all channel names to their `MuppetChannel` instances by calling `useMuppetChannels()`. Remember, these channel names will match exactly the strings passed to `MuppetProvider`, and may be null if they haven't attempted to connect yet.
### `sendEvent()`
For example, if you passed `channelNames={["some-channel", "another-channel"]}` to the overall `MuppetProvider`:
To broadcast a MUPPET event over your new MuppetChannel, simply pass your MUPPET eventClass and event to `MuppetChannel.sendEvent()`.
This can (and generally should) be invoked right in the Javascript component where the user took action. For example, the `onClick` callback of some button, or an `onSelect` of an HTML select element:
```javascript
const channelMap = useMuppetChannels();
const { 'some-channel': someChannel, 'another-channel': anotherChannel } = channelMap;
// MyComponent.jsx
import { useMuppetChannel } from '@noaa-gsl/idsse-muppet';

console.log('Is some-channel open?', someChannel.isOpen());
function MyComponent() {
const channel = useMuppetChannel('my-channel');

anotherChannel?.sendEvent({
eventClass: 'MY_APP.COLOR_SELECTED',
event: { color: 'blue' },
});
const onButtonClick = () => {
channel?.sendEvent({
eventClass: 'SOME_EVENT',
event: { value: 123 },
});
};

return <button onClick={onButtonClick}>Hello world</button>;
}

export default MyComponent;
```
## Send an RPC request
### `sendRequest()`
To send a MUPPET event that is expected to receive some response from the receiver, call `MuppetChannel.sendRequest()`, passing the eventClass, event body, and the destination (the name of the app that should respond to the event).
Expand Down Expand Up @@ -177,3 +205,19 @@ function MyComponent() {
```
Under the hood, this is sending a MUPPET event to the app "THEIR_APP" with an event class "MY_APP.GET_USER_PHONE", which the receiver should respond to by sending a MUPPET event back with class "THEIR_APP.GET_USER_PHONE" and destination: "MY_APP". The MUPPET library then resolves your awaited promise with the response payload.
### `sendRawEvent()`
This function should be avoided as much as possible, using `sendEvent()` instead, which will take the `event` you pass it and, using helpful defaults, transform it to a MUPPET protocol-compliant format that is understood by receiving apps (provide a UUID, build a lower-level eventClass that combines your clientName, eventClass, and the intended destination app, etc.)
If you need to bypass these MUPPET conventions, you can send a custom JSON object down the MUPPET channel. Be aware that it's not guaranteed to be parsed, interpreted, and acted upon as expected by the receiving app.
```javascript
const channel = useMuppetChannel('my-channel');
channel?.sendRawEvent({
id: '123456',
destination: 'OTHER_APP',
eventClass: 'CUSTOM_SENDER_APP.BUTTON_CLICKED',
event: { foo: 'bar' },
});
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@noaa-gsl/idsse-muppet",
"version": "0.0.6",
"version": "0.0.7",
"description": "Connect to, send, and receive data over MUPPET data channels in convenient React interfaces using the MUPPETs protocol",
"main": "index.js",
"scripts": {
Expand Down
4 changes: 3 additions & 1 deletion src/MuppetChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ class MuppetChannel {
id: crypto.randomUUID(),
destination,
requestId: requestId || undefined,
eventClass: `${this.#clientName}.${eventClass}`,
eventClass: eventClass.startsWith(`${this.#clientName}.`)
? eventClass
: `${this.#clientName}.${eventClass}`,
event,
});

Expand Down

0 comments on commit 97ce2db

Please sign in to comment.