Merge pull request #255 from status-im/store-guide

This commit is contained in:
Franck Royer 2021-08-09 14:56:06 +10:00 committed by GitHub
commit bc31089860
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 39127 additions and 167 deletions

View File

@ -70,8 +70,10 @@
"statusim",
"submodule",
"submodules",
"supercrypto",
"transpiled",
"typedoc",
"unmount",
"unmounts",
"untracked",
"upgrader",

View File

@ -12,7 +12,7 @@ jobs:
examples_build_and_test:
strategy:
matrix:
example: [ web-chat, eth-dm, min-react-js-chat ]
example: [ web-chat, eth-dm, min-react-js-chat, store-reactjs-chat ]
runs-on: ubuntu-latest
steps:

View File

@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- **Breaking**: The `WakuMessage` APIs have been changed to move `contentTopic` out of the optional parameters.
- **Breaking**: Move `contentTopics` out the `WakuStore.queryHistory`'s optional parameters.
- **Breaking**: `WakuStore.queryHistory` throws when encountering an error instead of returning a `null` value.
### Removed
- Examples (web-chat): Remove broken `/fleet` command.

View File

@ -118,20 +118,19 @@ Query a waku store peer to check historical messages:
```ts
// Process messages once they are all retrieved
const messages = await waku.store.queryHistory({ contentTopics: ["/my-cool-app/1/my-use-case/proto"] });
const messages = await waku.store.queryHistory(['/my-cool-app/1/my-use-case/proto']);
messages.forEach((msg) => {
console.log("Message retrieved:", msg.payloadAsUtf8)
})
console.log('Message retrieved:', msg.payloadAsUtf8);
});
// Or, pass a callback function to be executed as pages are received:
waku.store.queryHistory({
contentTopics: ["/my-cool-app/1/my-use-case/proto"],
callback: (messages) => {
messages.forEach((msg) => {
console.log("Message retrieved:", msg.payloadAsUtf8);
});
}
});
waku.store.queryHistory(['/my-cool-app/1/my-use-case/proto'], {
callback: (messages) => {
messages.forEach((msg) => {
console.log('Message retrieved:', msg.payloadAsUtf8);
});
}
});
```
### Encryption & Signature
@ -212,9 +211,8 @@ Keys can be removed using `WakuMessage.deleteDecryptionKey`.
##### Waku Store
```ts
const messages = await waku.store.queryHistory({
contentTopics: [],
decryptionKeys: [privateKey, symKey],
const messages = await waku.store.queryHistory([], {
decryptionKeys: [privateKey, symKey]
});
```
@ -228,20 +226,20 @@ In the case where your app does not need encryption then you could use symmetric
Signature keys can be generated the same way asymmetric keys for encryption are:
```ts
import { generatePrivateKey, getPublicKey, WakuMessage } from "js-waku";
import { generatePrivateKey, getPublicKey, WakuMessage } from 'js-waku';
const signPrivateKey = generatePrivateKey();
// Asymmetric Encryption
const message1 = await WakuMessage.fromBytes(payload, myAppContentTopic, {
encPublicKey: recipientPublicKey,
sigPrivKey: signPrivateKey,
sigPrivKey: signPrivateKey
});
// Symmetric Encryption
const message2 = await WakuMessage.fromBytes(payload, myAppContentTopic, {
encPublicKey: symKey,
sigPrivKey: signPrivateKey,
sigPrivKey: signPrivateKey
});
```

View File

@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
"name": "min-js-web-chat",
"version": "0.1.0",
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",

23
examples/store-reactjs-chat/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

38360
examples/store-reactjs-chat/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "store-reactjs-chat",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"js-waku": "../../build/main",
"protons": "^2.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts build",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,143 @@
import './App.css';
import { getStatusFleetNodes, StoreCodec, Waku } from 'js-waku';
import * as React from 'react';
import protons from 'protons';
const ContentTopic = '/toy-chat/2/huilong/proto';
const proto = protons(`
message ChatMessage {
uint64 timestamp = 1;
string nick = 2;
bytes text = 3;
}
`);
function App() {
const [waku, setWaku] = React.useState(undefined);
const [wakuStatus, setWakuStatus] = React.useState('None');
const [messages, setMessages] = React.useState([]);
// Set to true when Waku connects to a store node
// it does not reflect whether we then disconnected from said node.
const [connectedToStore, setConnectedToStore] = React.useState(false);
React.useEffect(() => {
if (!!waku) return;
if (wakuStatus !== 'None') return;
setWakuStatus('Starting');
Waku.create().then((waku) => {
setWaku(waku);
setWakuStatus('Connecting');
bootstrapWaku(waku).then(() => {
setWakuStatus('Ready');
});
});
}, [waku, wakuStatus]);
React.useEffect(() => {
if (!waku) return;
// This is superfluous as the try/catch block would catch the failure if
// we are indeed not connected to any store node.
if (!connectedToStore) return;
const interval = setInterval(() => {
waku.store
.queryHistory([ContentTopic])
.catch((e) => {
// We may not be connected to a store node just yet
console.log('Failed to retrieve messages', e);
})
.then((retrievedMessages) => {
const messages = retrievedMessages.map(decodeMessage).filter(Boolean);
setMessages(messages);
});
}, 10000);
return () => clearInterval(interval);
}, [waku, connectedToStore]);
React.useEffect(() => {
if (!waku) return;
// We do not handle disconnection/re-connection in this example
if (connectedToStore) return;
const isStoreNode = ({ protocols }) => {
if (protocols.includes(StoreCodec)) {
// We are now connected to a store node
setConnectedToStore(true);
}
};
// This demonstrates how to wait for a connection to a store node.
//
// This is only for demonstration purposes. It is not really needed in this
// example app as we query the store node every 10s and catch if it fails.
// Meaning if we are not connected to a store node, then it just fails and
// we try again 10s later.
waku.libp2p.peerStore.on('change:protocols', isStoreNode);
return () => {
waku.libp2p.peerStore.removeListener('change:protocols', isStoreNode);
};
}, [waku, connectedToStore]);
return (
<div className="App">
<header className="App-header">
<h2>{wakuStatus}</h2>
<h3>Messages</h3>
<ul>
<Messages messages={messages} />
</ul>
</header>
</div>
);
}
export default App;
async function bootstrapWaku(waku) {
const nodes = await getStatusFleetNodes();
await Promise.all(nodes.map((addr) => waku.dial(addr)));
}
function decodeMessage(wakuMessage) {
if (!wakuMessage.payload) return;
const { timestamp, nick, text } = proto.ChatMessage.decode(
wakuMessage.payload
);
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 };
}
function Messages(props) {
return props.messages.map(({ text, timestamp, nick }) => {
return (
<li>
({formatDate(timestamp)}) {nick}: {text}
</li>
);
});
}
function formatDate(timestamp) {
return timestamp.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: false,
});
}

View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -62,14 +62,18 @@ async function retrieveStoreMessages(
setArchivedMessages(messages);
};
const res = await waku.store.queryHistory({
contentTopics: [ChatContentTopic],
pageSize: 5,
direction: Direction.FORWARD,
callback,
});
try {
const res = await waku.store.queryHistory([ChatContentTopic], {
pageSize: 5,
direction: Direction.FORWARD,
callback,
});
return res ? res.length : 0;
return res.length;
} catch {
console.log('Failed to retrieve messages');
return 0;
}
}
export default function App() {
@ -118,29 +122,29 @@ export default function App() {
if (!waku) return;
if (historicalMessagesRetrieved) return;
const connectedToStorePeer = new Promise((resolve) =>
waku.libp2p.peerStore.once(
'change:protocols',
({ peerId, protocols }) => {
if (protocols.includes(StoreCodec)) {
resolve(peerId);
}
const checkAndRetrieve = ({ protocols }: { protocols: string[] }) => {
if (protocols.includes(StoreCodec)) {
console.log(`Retrieving archived messages}`);
setHistoricalMessagesRetrieved(true);
try {
retrieveStoreMessages(waku, dispatchMessages).then((length) =>
console.log(`Messages retrieved:`, length)
);
} catch (e) {
console.log(`Error encountered when retrieving archived messages`, e);
}
)
);
connectedToStorePeer.then(() => {
console.log(`Retrieving archived messages}`);
setHistoricalMessagesRetrieved(true);
try {
retrieveStoreMessages(waku, dispatchMessages).then((length) =>
console.log(`Messages retrieved:`, length)
);
} catch (e) {
console.log(`Error encountered when retrieving archived messages`, e);
}
});
};
waku.libp2p.peerStore.on('change:protocols', checkAndRetrieve);
return () => {
waku.libp2p.peerStore.removeListener(
'change:protocols',
checkAndRetrieve
);
};
}, [waku, historicalMessagesRetrieved]);
return (

View File

@ -14,7 +14,7 @@ The format for content topics is as follows:
- `content-topic-name`: The actual content topic name to use for filtering.
If your dApp uses DappConnect for several features,
you should use a content topic per feature.
- `encoding`: The encoding format of the message, we recommend using Protobuf: `proto`.
- `encoding`: The encoding format of the message, Protobuf is most often used: `proto`.
For example: Your dApp's name is SuperCrypto,
it enables users to receive notifications and send private messages.

View File

@ -3,3 +3,4 @@
- [Receive and Send Messages Using Waku Relay](relay-receive-send-messages.md)
- [How to Choose a Content Topic](choose-content-topic.md)
- [Receive and Send Messages Using Waku Relay With ReactJS](reactjs-relay.md)
- [Retrieve Messages Using Waku Store](store-retrieve-messages.md)

View File

@ -270,8 +270,8 @@ function App() {
time.setTime(timestamp);
const message = { text, timestamp: time };
setMessages((currMessages) => {
return [message].concat(currMessages);
setMessages((messages) => {
return [message].concat(messages);
});
}, []);

View File

@ -75,10 +75,12 @@ await waku.relay.send(wakuMessage);
# Use Protobuf
Sending strings as messages in unlikely to cover your dApps needs.
To include structured objects in Waku Messages,
we recommend you use [protobuf](https://developers.google.com/protocol-buffers/).
First, let's define a data structure.
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 include structured objects in Waku Messages.
First, define a data structure.
For this guide, we will use a simple chat message that contains a timestamp and text:
```js
@ -107,7 +109,7 @@ import protons from 'protons';
const proto = protons(`
message SimpleChatMessage {
float timestamp = 1;
uint64 timestamp = 1;
string text = 2;
}
`);
@ -180,7 +182,7 @@ import protons from 'protons';
const proto = protons(`
message SimpleChatMessage {
float timestamp = 1;
uint64 timestamp = 1;
string text = 2;
}
`);

View File

@ -0,0 +1,170 @@
# Retrieve Messages Using Waku Store
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.
For this guide, we are using a single content topic: `/store-guide/1/news/proto`.
# Installation
You can install [js-waku](https://npmjs.com/package/js-waku) using your favorite package manager:
```shell
npm install js-waku
```
# Create Waku Instance
In order to interact with the Waku network, you first need a Waku instance:
```js
import { Waku } from 'js-waku';
const wakuNode = await Waku.create();
```
# Connect to Other Peers
The Waku instance needs to connect to other peers to communicate with the network.
You are free to choose other methods to bootstrap and DappConnect will ship with new bootstrap mechanisms in the future.
For now, the easiest way is to connect to Status' Waku fleet:
```js
import { getStatusFleetNodes } from 'js-waku';
const nodes = await getStatusFleetNodes();
await Promise.all(nodes.map((addr) => waku.dial(addr)));
```
# 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 send structured data.
First, define a data structure.
For this guide, we will use a simple news article that contains a date of publication, title and body:
```js
{
date: Date;
title: string;
body: string;
}
```
To encode and decode protobuf payloads, you can use the [protons](https://www.npmjs.com/package/protons) package.
## Install Protobuf Library
First, install protons:
```shell
npm install protons
```
## Protobuf Definition
Then specify the data structure:
```js
import protons from 'protons';
const proto = protons(`
message ArticleMessage {
uint64 date = 1;
string title = 2;
string body = 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
const decodeWakuMessage = (wakuMessage) => {
// No need to attempt to decode a message if the payload is absent
if (!wakuMessage.payload) return;
const { date, title, body } = proto.SimpleChatMessage.decode(
wakuMessage.payload
);
// In protobuf, fields are optional so best to check
if (!date || !title || !body) return;
const publishDate = new Date();
publishDate.setTime(date);
return { publishDate, title, body };
};
```
## Retrieve messages
You now have all the building blocks to retrieve and decode messages for a store node.
Retrieve messages from a store node:
```js
const ContentTopic = '/store-guide/1/news/proto';
waku.store
.queryHistory([ContentTopic])
.catch((e) => {
// Be sure to catch any potential error
console.log('Failed to retrieve messages', e);
})
.then((retrievedMessages) => {
const articles = retrievedMessages
.map(decodeWakuMessage) // Decode messages
.filter(Boolean); // Filter out undefined values
console.log(`${articles.length} articles have been retrieved`);
});
```
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.
## Wait to be connected
Depending on your dApp design, you may want to wait for a store node to be available first.
In this case, you can listen for the [PeerStore's change protocol event](https://github.com/libp2p/js-libp2p/blob/master/doc/API.md#known-protocols-for-a-peer-change)
to know whether any of your connected peers is a store peer:
```js
import { StoreCodec } from 'js-waku';
// Or using a callback
waku.libp2p.peerStore.on('change:protocols', ({ peerId, protocols }) => {
if (protocols.includes(StoreCodec)) {
// A Store node is available!
}
});
```

View File

@ -29,7 +29,7 @@
"test": "run-s build test:*",
"test:lint": "eslint src --ext .ts",
"test:prettier": "prettier \"src/**/*.ts\" \"./*.json\" \"*.conf.js\" --list-different",
"test:spelling": "cspell \"{README.md,.github/*.md,src/**/*.ts}\"",
"test:spelling": "cspell \"{README.md,.github/*.md,guides/*.md,src/**/*.ts}\"",
"test:unit": "nyc --silent mocha",
"test:karma": "karma start",
"examples:test": "run-s examples:pretest; for d in examples/*; do (cd $d; npm test;); done",

View File

@ -8,7 +8,7 @@ export enum Direction {
FORWARD = 'forward',
}
export interface Options {
export interface Params {
contentTopics: string[];
cursor?: proto.Index;
pubsubTopic: string;
@ -22,22 +22,22 @@ export class HistoryRPC {
/**
* Create History Query.
*/
static createQuery(options: Options): HistoryRPC {
const direction = directionToProto(options.direction);
static createQuery(params: Params): HistoryRPC {
const direction = directionToProto(params.direction);
const pagingInfo = {
pageSize: options.pageSize,
cursor: options.cursor,
pageSize: params.pageSize,
cursor: params.cursor,
direction,
};
const contentFilters = options.contentTopics.map((contentTopic) => {
const contentFilters = params.contentTopics.map((contentTopic) => {
return { contentTopic };
});
return new HistoryRPC({
requestId: uuid(),
query: {
pubsubTopic: options.pubsubTopic,
pubsubTopic: params.pubsubTopic,
contentFilters,
pagingInfo,
startTime: undefined,

View File

@ -55,9 +55,7 @@ describe('Waku Store', () => {
waku.libp2p.peerStore.once('change:protocols', resolve);
});
const messages = await waku.store.queryHistory({
contentTopics: [],
});
const messages = await waku.store.queryHistory([]);
expect(messages?.length).eq(2);
const result = messages?.findIndex((msg) => {
@ -91,8 +89,7 @@ describe('Waku Store', () => {
waku.libp2p.peerStore.once('change:protocols', resolve);
});
const messages = await waku.store.queryHistory({
contentTopics: [],
const messages = await waku.store.queryHistory([], {
direction: Direction.FORWARD,
});
@ -136,9 +133,8 @@ describe('Waku Store', () => {
const nimPeerId = await nimWaku.getPeerId();
const messages = await waku.store.queryHistory({
const messages = await waku.store.queryHistory([], {
peerId: nimPeerId,
contentTopics: [],
});
expect(messages?.length).eq(2);
@ -237,8 +233,7 @@ describe('Waku Store', () => {
}
dbg('Retrieve messages from store');
const messages = await waku2.store.queryHistory({
contentTopics: [],
const messages = await waku2.store.queryHistory([], {
decryptionKeys: [privateKey, symKey],
});

View File

@ -33,7 +33,6 @@ export interface CreateOptions {
export interface QueryOptions {
peerId?: PeerId;
contentTopics: string[];
pubsubTopic?: string;
direction?: Direction;
pageSize?: number;
@ -58,26 +57,30 @@ export class WakuStore {
/**
* Query given peer using Waku Store.
*
* @param contentTopics The content topics to pass to the query, leave empty to
* retrieve all messages.
* @param options
* @param options.peerId The peer to query.Options
* @param options.contentTopics The content topics to pass to the query, leave empty to
* retrieve all messages.
* @param options.pubsubTopic The pubsub topic to pass to the query. Defaults
* to the value set at creation. See [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/).
* @param options.callback Callback called on page of stored messages as they are retrieved
* @param options.decryptionKeys Keys that will be used to decrypt messages.
* It can be Asymmetric Private Keys and Symmetric Keys in the same array, all keys will be tried with both
* methods.
* @throws If not able to reach the peer to query.
* @throws If not able to reach the peer to query or error when processing the reply.
*/
async queryHistory(options: QueryOptions): Promise<WakuMessage[] | null> {
async queryHistory(
contentTopics: string[],
options?: QueryOptions
): Promise<WakuMessage[]> {
const opts = Object.assign(
{
pubsubTopic: this.pubsubTopic,
direction: Direction.BACKWARD,
pageSize: 10,
},
options
options,
{ contentTopics }
);
dbg('Querying history with the following options', options);
@ -97,98 +100,78 @@ export class WakuStore {
const messages: WakuMessage[] = [];
let cursor = undefined;
while (true) {
try {
const { stream } = await connection.newStream(StoreCodec);
try {
const queryOpts = Object.assign(opts, { cursor });
const historyRpcQuery = HistoryRPC.createQuery(queryOpts);
const res = await pipe(
[historyRpcQuery.encode()],
lp.encode(),
stream,
lp.decode(),
concat
const { stream } = await connection.newStream(StoreCodec);
const queryOpts = Object.assign(opts, { cursor });
const historyRpcQuery = HistoryRPC.createQuery(queryOpts);
const res = await pipe(
[historyRpcQuery.encode()],
lp.encode(),
stream,
lp.decode(),
concat
);
const reply = HistoryRPC.decode(res.slice());
const response = reply.response;
if (!response) {
throw 'History response misses response field';
}
if (
response.error &&
response.error === HistoryResponse_Error.ERROR_INVALID_CURSOR
) {
throw 'History response contains an Error: INVALID CURSOR';
}
if (!response.messages || !response.messages.length) {
// No messages left (or stored)
console.log('No messages present in HistoryRPC response');
return messages;
}
dbg(
`${response.messages.length} messages retrieved for pubsub topic ${opts.pubsubTopic}`
);
const pageMessages: WakuMessage[] = [];
await Promise.all(
response.messages.map(async (protoMsg) => {
const msg = await WakuMessage.decodeProto(
protoMsg,
opts.decryptionKeys
);
try {
const reply = HistoryRPC.decode(res.slice());
const response = reply.response;
if (!response) {
console.log('No response in HistoryRPC');
return null;
}
if (
response.error &&
response.error === HistoryResponse_Error.ERROR_INVALID_CURSOR
) {
console.log('Error in response: INVALID CURSOR');
return null;
}
if (!response.messages || !response.messages.length) {
// No messages left (or stored)
console.log('No messages present in HistoryRPC response');
return messages;
}
dbg(
`${response.messages.length} messages retrieved for pubsub topic ${opts.pubsubTopic}`
);
const pageMessages: WakuMessage[] = [];
await Promise.all(
response.messages.map(async (protoMsg) => {
const msg = await WakuMessage.decodeProto(
protoMsg,
opts.decryptionKeys
);
if (msg) {
messages.push(msg);
pageMessages.push(msg);
}
})
);
if (opts.callback) {
// TODO: Test the callback feature
// TODO: Change callback to take individual messages
opts.callback(pageMessages);
}
const responsePageSize = response.pagingInfo?.pageSize;
const queryPageSize = historyRpcQuery.query?.pagingInfo?.pageSize;
if (
responsePageSize &&
queryPageSize &&
responsePageSize < queryPageSize
) {
// Response page size smaller than query, meaning this is the last page
return messages;
}
cursor = response.pagingInfo?.cursor;
if (cursor === undefined) {
// If the server does not return cursor then there is an issue,
// Need to abort or we end up in an infinite loop
console.log('No cursor returned by peer.');
return messages;
}
} catch (err) {
console.log('Failed to decode store reply', err);
return null;
if (msg) {
messages.push(msg);
pageMessages.push(msg);
}
} catch (err) {
console.log('Failed to send waku store query', err);
return null;
}
} catch (err) {
console.log(
'Failed to negotiate waku store protocol stream with peer',
err
);
return null;
})
);
if (opts.callback) {
// TODO: Test the callback feature
// TODO: Change callback to take individual messages
opts.callback(pageMessages);
}
const responsePageSize = response.pagingInfo?.pageSize;
const queryPageSize = historyRpcQuery.query?.pagingInfo?.pageSize;
if (
responsePageSize &&
queryPageSize &&
responsePageSize < queryPageSize
) {
// Response page size smaller than query, meaning this is the last page
return messages;
}
cursor = response.pagingInfo?.cursor;
if (cursor === undefined) {
// If the server does not return cursor then there is an issue,
// Need to abort or we end up in an infinite loop
console.log('No cursor returned by peer.');
return messages;
}
}
}