I'm using Chainlink Oracles to get random numbers. This process of getting a random number takes a while. In order to do that, I have to execute this line (asynchronous call), which calls a function of my smart contract an stores the new random number:
const newRandomNumber = await contract.connect(signer).randomResult();
What I want is to automate this process: when a new number is generated (and it is available for use), I would like to display a message to the user (in my fronted).
Hey, a new number has been generated.
How could I periodically check if the result stored in newRandomNumber has changed? And how could I display a message when that has happened?
Create an Observable to periodically call an API-endpoint:
import { distinctUntilChanged, switchMap, timer } form 'rxjs'; /* Use RxJS library for convenience */
const getRandomNumber = contract.connect(signer).randomResult();
const observable = timer(0, 1000) /* Trigger now and each 1000 ms */
.pipe(
switchMap(getRandomNumber), /* On each run, get the current number */
distinctUntilChanged(), /* Only trigger subscribers if new number */
)
const subscription = observable.subscribe((number) => console.log(number));
/* Don't forget to unsubscribe after, e.g. when the component unmounts */
subscription.unsubscribe();
The observer pattern allows you to receive notifications (e.i. 'subscribe') to multiple asynchronous events - the same way a javascript Promise notifies you after a single asynchronous event finished. (Observers are actually much more flexible than that, this is merely one usecase)
Let's see how we could implement your desired behavior in basic javascript
let currentRandomNumber = null;
let isCanceled = false;
async function checkAndUpdateNumber() {
while (!isCanceled) {
/* Set your new (or same) number */
currentRandomNumber = await contract.connect(signer).randomResult();
/* Wait 1000ms */
await new Promsie((resolve) => setTimeout(resolve, 1000));
}
}
checkAndUpdateNumber();
/* If you want to stop the loop */
isCancelled = true;
This implementation works but leaves much room for improvement. The code is not reusable in the slightest and is not easily testable.
Observers provide you with a much cleaner interface to handle multiple asynchronous operations. Check out this article to get an idea of how observers work under the hood.
The go-to javascript library for observables is RxJS. It is well tested and provides countless utility methods so I'd highly recommend you to check it out.
Because you tagged React on your question, here's a commented example using React state:
<div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.16.6/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useEffect, useState} = React;
// simulate making a network request to get a random number from an API:
// it will return a promise which will (at some point in the future)
// resolve with the random number, or reject with an Error
function mockFetchRandomNumber () {
const randomNumber = Math.floor(Math.random() * 1000) + 1; // 1-1000
const delay = Math.floor(Math.random() * 1000) + 1000; // 1-2s
const chance = 0.2; // percent chance of random network failure
return new Promise((resolve, reject) => setTimeout(() => {
if (chance > Math.random()) reject(new Error('Network error'));
else resolve(randomNumber);
}, delay));
}
// a custom hook to wrap the logic of fetching the random number
// and maintain the state of the request while doing so
function useRandomNumber () {
const [data, setData] = useState(); // the potential data
const [error, setError] = useState(); // a potential error
const [isLoading, setIsLoading] = useState(false); // whether the request is still pending
const [reloadBoolean, setReloadBoolean] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setData(undefined);
setError(undefined);
try {
const randomNumber = await mockFetchRandomNumber();
setData(randomNumber);
setError(undefined);
}
catch (ex) {
setError(ex instanceof Error ? ex : new Error(String(ex)));
setData(undefined);
}
setIsLoading(false);
};
fetchData();
}, [reloadBoolean, setData, setError, setIsLoading]);
return {
data,
error,
isLoading,
// will toggle the value of `reloadBoolean`, forcing a re-render
reload: () => setReloadBoolean(b => !b),
};
}
function Example () {
const {data, error, isLoading, reload} = useRandomNumber();
return (
<div>
{
isLoading
? (<div>Loading random number...</div>)
: null
}
{
error
? (<div>There was an error loading the data ({error.message})</div>)
: null
}
{
data
? (<div>The random number is: {data}</div>)
: null
}
<button onClick={reload}>Get new number</button>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>