Real Time Server Updates
At this point we've successfully connected our user interface to a REST server! However what happens if a new channel is created or a new node creates a channel? Our Lightning Network nodes will have new graph information but we would need to manually refresh the page.
Go ahead and give it a try by creating a channel between Bob and Carol. When we refresh the browser we should see a new link between Bob and Carol.
This is ok, but we can do better by passing updates to our user interface using WebSockets.
Exploring WebSocket Code
The WebSocket code on our server uses the ws library and lives inside the SocketServer
class. You don't have to make any changes to it, but you may want to take a look at it. This class maintains a set of connected sockets. It also includes a broadcast
method that allows us to send data for some channel to all connected sockets. We'll use this broadcast
method shortly to send graph updates to all connected WebSockets.
The code to start the SocketServer
lives inside Server
. At the end of the run
method, we create the SocketServer
instance and have it listen to the HTTP server for connections.
// server/src/Server
async function run() {
// OTHER CODE IS HERE...
// start the server on the port
const server = app.listen(Number(options.port), () => {
console.log(`server listening on ${options.port}`);
});
// start the socket server
const socketServer = new SocketServer();
// start listening for http connections
socketServer.listen(server);
All of this is ready to go, all we need to do is subscribe to updates from LND and do something with them.
Exercise: Subscribe to Updates
Back in our server code's LndGraphService
is a method subscribeGraph
that we need to implement. This method subscribes to graph updates from LND using it's subscribeGraph
method. For each update, we will emit an event named update
and supply the update value we received from LND.
Dev Note: This class is an EventEmitter. EventEmitters can use the emit
method to tell other classes that something has happened. For example: this.emit("my_event", "something happened")
. The value(s) passed as arguments to the emit
method will be supplied to each of the observers. These other classes are "observers" and can listen using the on
method for the named event such as "my_event". Using EventEmitters allows us to keep code decoupled and avoid messy callback nesting.
// server/src/domain/lnd/LndGraphService
public async subscribeGraph(): Promise<void> {
// Exercise: subscribe to the Lnd graph updates using `this.lnd.subscribeGraph`
// and emit a "update" event each time the handler is called using `this.emit`
return this.lnd.subscribeGraph((update: Lnd.GraphUpdate) => {
// Todo
});
}
Exploring WebSocket Broadcasting
The next logical step is consuming the update
event that we just created and sending the update to the client over a WebSocket. If you navigate back to the trusty Server
you will find some interesting code at the bottom of the run
function.
// server/src/Server
async function run() {
// other code is here...
// construct the socket server
const socketServer = new SocketServer();
// start listening for http connections using the http server
socketServer.listen(server);
// attach an event handler for graph updates and broadcast them
// to WebSocket using the socketServer.
graphAdapter.on("update", (update: Lnd.GraphUpdate) => {
socketServer.broadcast("graph", update);
});
// subscribe to graph updates
graphAdapter.subscribeGraph();
}
We subscribe to the update
event on graphAdapter
that we just implemented. In the event handler we then broadcast the update to all of the WebSockets.
After the event handler is defined, all of the plumbing is in place to for updates to go from LND -> LndRestClient -> LndGraphAdapter -> WebSocket
.
You should now be able to connect a WebSocket to the server and receive updates by generating channel opens or closes in Polar.
Real Time User Interface
Now that our WebSocket server is sending updates, we need to wire these updates into our user interface.
Exploring Socket Connectivity
The application already has some code to help us. We use React's context to establish a long-lived WebSocket that can be used by any component in the component hierarchy. This code lives in client/src/context/SocketContext
.
To integrate this context into our components we can use a custom hook: useSocket
that lives in client/src/hooks/UseSocket
. This hook allows us to retrieve the websocket and subscribe to events for a any channel.
For example:
export const SomeComponent = () => {
const socket = useSocket("some_channel", (data) => {
// do something with data
console.log(data);
});
};
The last thing we should know is that in order for this to work, we need to establish the React Context higher in the component hierarchy. A great place is at the root!. We add the context via the SocketProvider
component in our application's root component: App
.
// client/src/App
import React from "react";
import ReactDom from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { SocketProvider } from "./context/SocketContext";
import { LayoutScene } from "./scenes/layout/LayoutScene";
ReactDom.render(
<SocketProvider>
<BrowserRouter>
<LayoutScene />
</BrowserRouter>
</SocketProvider>,
document.getElementById("app")
);
With the lay of the land defined, we can now embark on our journey to finish the real time updates.
Exercise: Subscribe to Updates
The logical place to subscribe to updates is in the GraphScene
component. As previously established, this scene is responsible for wiring up data connections for graph related components.
Pointing our IDE at the GraphScene
component our next exercise is implementing the socket handler. Using the useSocket
hook, subscribe to graph
channel. The handler function should call the graphRef.current.updateGraph
method on the graph component.
// client/src/scenes/graph/GraphScene
import React, { useEffect, useRef } from "react";
import { useSocket } from "../../hooks/UseSocket";
import { useApi } from "../../hooks/UseApi";
import { Graph } from "./components/Graph";
export const GraphScene = () => {
const api = useApi();
const graphRef = useRef<Graph>();
useEffect(() => {
api.fetchGraph().then((graph) => {
console.log("received graph", graph);
graphRef.current.createGraph(graph);
});
}, []);
useSocket("graph", (update: Lnd.GraphUpdate) => {
// Exercise: Call `graphRef.current.updateGraph` with the update
});
return (
<div className="container-fluid h-100">
<div className="row h-100">
<div className="col h-100">{<Graph ref={graphRef} />}</div>
</div>
</div>
);
};
Calling the updateGraph
method converts the Lnd.GraphUpdate
object into D3Node
and D3Link
objects. The Lnd.GraphUpdate
object we receive from the server is defined in server/src/domain/lnd/LndRestTypes
. It consists of four pieces of data that we care about:
- new nodes that are don't yet have in the graph
- existing nodes that need to have their title and alias updated
- new channels that we need to add to the graph
- closed channels that we need to remove from the graph
After completing this exercise we will have everything needed for our graph to be functional. Try adding or removing a channel, you should see our graph application automatically update with the changes! Keep in mind that it may take a moment for changes to propagate throughout your network.