mirror of https://github.com/waku-org/js-waku.git
297 lines
8.0 KiB
Markdown
297 lines
8.0 KiB
Markdown
# Retrieve Messages Using Waku Store With ReactJS
|
|
|
|
It is easy to use DappConnect with ReactJS.
|
|
In this guide, we will demonstrate how your ReactJS dApp can use Waku Store to retrieve messages.
|
|
|
|
DApps running on a phone or in a browser are often offline:
|
|
The browser could be closed or mobile app in the background.
|
|
|
|
[Waku Relay](https://rfc.vac.dev/spec/18/) is a gossip protocol.
|
|
As a user, it means that your peers forward you messages they just received.
|
|
If you cannot be reached by your peers, then messages are not relayed;
|
|
relay peers do **not** save messages for later.
|
|
|
|
However, [Waku Store](https://rfc.vac.dev/spec/13/) peers do save messages they relay,
|
|
allowing you to retrieve them at a later time.
|
|
The Waku Store protocol is best-effort and does not guarantee data availability.
|
|
Waku Relay should still be preferred when online;
|
|
Waku Store can be used after resuming connectivity:
|
|
For example, when the dApp starts.
|
|
|
|
In this guide, we'll review how you can use Waku Store to retrieve messages.
|
|
|
|
Before starting, you need to choose a _Content Topic_ for your dApp.
|
|
Check out the [how to choose a content topic guide](choose-content-topic.md) to learn more about content topics.
|
|
|
|
# Setup
|
|
|
|
Create a new React app:
|
|
|
|
```shell
|
|
npx create-react-app my-app
|
|
cd my-app
|
|
```
|
|
|
|
Then, install [js-waku](https://npmjs.com/package/js-waku):
|
|
|
|
```shell
|
|
npm install js-waku
|
|
```
|
|
|
|
Start the dev server and open the dApp in your browser:
|
|
|
|
```shell
|
|
npm run start
|
|
```
|
|
|
|
Note: We have noticed some issues with React bundling due to `npm` pulling an old version of babel.
|
|
If you are getting an error about the [optional chaining (?.)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)
|
|
character not being valid, try cleaning up and re-installing your dependencies:
|
|
|
|
```shell
|
|
rm -rf node_modules package-lock.json
|
|
npm install
|
|
```
|
|
|
|
# Create Waku Instance
|
|
|
|
In order to interact with the Waku network, you first need a Waku instance.
|
|
Go to `App.js` and modify the `App` function:
|
|
|
|
```js
|
|
import { Waku } from 'js-waku';
|
|
import * as React from 'react';
|
|
|
|
function App() {
|
|
const [waku, setWaku] = React.useState(undefined);
|
|
const [wakuStatus, setWakuStatus] = React.useState('None');
|
|
|
|
// Start Waku
|
|
React.useEffect(() => {
|
|
// If Waku status not None, it means we are already starting Waku
|
|
if (wakuStatus !== 'None') return;
|
|
|
|
setWakuStatus('Starting');
|
|
|
|
// Create Waku
|
|
Waku.create({ bootstrap: true }).then((waku) => {
|
|
// Once done, put it in the state
|
|
setWaku(waku);
|
|
// And update the status
|
|
setWakuStatus('Connecting');
|
|
});
|
|
}, [waku, wakuStatus]);
|
|
|
|
return (
|
|
<div className='App'>
|
|
<header className='App-header'>
|
|
// Display the status on the web page
|
|
<p>{wakuStatus}</p>
|
|
</header>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
# Wait to be connected
|
|
|
|
When using the `bootstrap` option, it may take some time to connect to other peers.
|
|
To ensure that you have store peers available to retrieve messages from,
|
|
use the `Waku.waitForConnectedPeer()` async function:
|
|
|
|
```js
|
|
React.useEffect(() => {
|
|
if (!waku) return;
|
|
|
|
if (wakuStatus === 'Connected') return;
|
|
|
|
waku.waitForConnectedPeer().then(() => {
|
|
setWakuStatus('Connected');
|
|
});
|
|
}, [waku, wakuStatus]);
|
|
```
|
|
|
|
# Use Protobuf
|
|
|
|
Waku v2 protocols use [protobuf](https://developers.google.com/protocol-buffers/) [by default](https://rfc.vac.dev/spec/10/).
|
|
|
|
Let's review how you can use protobuf to decode structured data.
|
|
|
|
First, define a data structure.
|
|
For this guide, we will use a simple chat message that contains a timestamp, nick and text:
|
|
|
|
```js
|
|
{
|
|
timestamp: Date;
|
|
nick: string;
|
|
text: string;
|
|
}
|
|
```
|
|
|
|
To encode and decode protobuf payloads, you can use the [protons](https://www.npmjs.com/package/protons) package.
|
|
|
|
## Install Protobuf Library
|
|
|
|
```shell
|
|
npm install protons
|
|
```
|
|
|
|
## Protobuf Definition
|
|
|
|
Define the data structure with protons:
|
|
|
|
```js
|
|
import protons from 'protons';
|
|
|
|
const proto = protons(`
|
|
message ChatMessage {
|
|
uint64 timestamp = 1;
|
|
string nick = 2;
|
|
bytes text = 3;
|
|
}
|
|
`);
|
|
```
|
|
|
|
You can learn about protobuf message definitions here:
|
|
[Protocol Buffers Language Guide](https://developers.google.com/protocol-buffers/docs/proto).
|
|
|
|
## Decode Messages
|
|
|
|
To decode the messages retrieved from a Waku Store node,
|
|
you need to extract the protobuf payload and decode it using `protons`.
|
|
|
|
```js
|
|
function decodeMessage(wakuMessage) {
|
|
if (!wakuMessage.payload) return;
|
|
|
|
const { timestamp, nick, text } = proto.ChatMessage.decode(
|
|
wakuMessage.payload
|
|
);
|
|
|
|
// All fields in protobuf are optional so be sure to check
|
|
if (!timestamp || !text || !nick) return;
|
|
|
|
const time = new Date();
|
|
time.setTime(timestamp);
|
|
|
|
const utf8Text = Buffer.from(text).toString('utf-8');
|
|
|
|
return { text: utf8Text, timestamp: time, nick };
|
|
}
|
|
|
|
```
|
|
|
|
## Retrieve messages
|
|
|
|
You now have all the building blocks to retrieve and decode messages for a store node.
|
|
|
|
Note that Waku Store queries are paginated.
|
|
The API provided by `js-waku` automatically traverses all pages of the Waku Store response.
|
|
By default, the most recent page is retrieved first but this can be changed with the `pageDirection` option.
|
|
|
|
First, define a React state to save the messages:
|
|
|
|
```js
|
|
function App() {
|
|
const [messages, setMessages] = React.useState([]);
|
|
/// [..]
|
|
}
|
|
```
|
|
|
|
Then, define `processMessages` to decode and then store messages in the React state.
|
|
You will pass `processMessages` as a `callback` option to `WakuStore.queryHistory`.
|
|
`processMessages` will be called each time a page is received from the Waku Store.
|
|
|
|
```js
|
|
const processMessages = (retrievedMessages) => {
|
|
const messages = retrievedMessages.map(decodeMessage).filter(Boolean);
|
|
|
|
setMessages((currentMessages) => {
|
|
return currentMessages.concat(messages.reverse());
|
|
});
|
|
};
|
|
```
|
|
|
|
Finally, pass `processMessage` in `WakuStore.queryHistory` as the `callback` value:
|
|
|
|
```js
|
|
waku.store
|
|
.queryHistory([ContentTopic], { callback: processMessages });
|
|
```
|
|
|
|
All together, you should now have:
|
|
|
|
```js
|
|
const ContentTopic = '/toy-chat/2/huilong/proto';
|
|
|
|
function App() {
|
|
// [..]
|
|
// Store messages in the state
|
|
const [messages, setMessages] = React.useState([]);
|
|
|
|
React.useEffect(() => {
|
|
if (wakuStatus !== 'Connected') return;
|
|
|
|
const processMessages = (retrievedMessages) => {
|
|
const messages = retrievedMessages.map(decodeMessage).filter(Boolean);
|
|
|
|
setMessages((currentMessages) => {
|
|
return currentMessages.concat(messages.reverse());
|
|
});
|
|
};
|
|
|
|
waku.store
|
|
.queryHistory([ContentTopic], { callback: processMessages })
|
|
.catch((e) => {
|
|
console.log('Failed to retrieve messages', e);
|
|
});
|
|
}, [waku, wakuStatus]);
|
|
|
|
return (
|
|
<div className='App'>
|
|
<header className='App-header'>
|
|
<h2>{wakuStatus}</h2>
|
|
<h3>Messages</h3>
|
|
<ul>
|
|
<Messages messages={messages} />
|
|
</ul>
|
|
</header>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
Note that `WakuStore.queryHistory` select an available store node for you.
|
|
However, it can only select a connected node, which is why the bootstrapping is necessary.
|
|
It will throw an error if no store node is available.
|
|
|
|
## Filter messages by send time
|
|
|
|
By default, Waku Store nodes store messages for 30 days.
|
|
Depending on your use case, you may not need to retrieve 30 days worth of messages.
|
|
|
|
[Waku Message](https://rfc.vac.dev/spec/14/) defines an optional unencrypted `timestamp` field.
|
|
The timestamp is set by the sender.
|
|
By default, js-waku [sets the timestamp of outgoing message to the current time](https://github.com/status-im/js-waku/blob/a056227538f9409aa9134c7ef0df25f602dbea58/src/lib/waku_message/index.ts#L76).
|
|
|
|
You can filter messages that include a timestamp within given bounds with the `timeFilter` option.
|
|
|
|
Retrieve messages up to a week old:
|
|
|
|
```js
|
|
const startTime = new Date();
|
|
// 7 days/week, 24 hours/day, 60min/hour, 60secs/min, 100ms/sec
|
|
startTime.setTime(startTime.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
waku.store
|
|
.queryHistory([ContentTopic], {
|
|
callback: processMessages,
|
|
timeFilter: { startTime, endTime: new Date() }
|
|
});
|
|
```
|
|
|
|
## End result
|
|
|
|
You can see the complete code in the [Minimal ReactJS Waku Store App](/examples/store-reactjs-chat).
|