This application demonstrates the use of Fastly Fanout to maintain a leaderboard and update it across devices in real time.
A live instance of this demo can be found at leaderboard-demo.edgecompute.app.
To enable realtime updates, Fastly Fanout is positioned as a GRIP (Generic Realtime Intermediary Protocol) proxy. Responses for streaming requests are held open by Fanout. Then, as updates become ready, the backend application publishes these updates through Fanout to all connected clients. For details on this mechanism, see Real-time Updates below.
The project comprises two main parts:
-
A web application. The backend for this web application is written in JavaScript, for Node.js. It uses the Express web framework and uses SQLite to maintain a small database. The frontend for this web application is a React application that is bundled using Webpack. The bundle is served by the backend as a static file.
-
An edge application. A Fastly Compute application that passes traffic through to the web application, and activates the Fanout feature for relevant requests.
The live instance's backend runs on Glitch, and the project can be viewed here: https://glitch.com/~fanout-leaderboard-demo.
The live instance's edge application is at leaderboard-demo.edgecompute.app. It is configured with the above Glitch application as the backend, and the service has the Fanout feature enabled.
For more details, see the Architecture section below.
Though the project is designed with Glitch and Fastly in mind, it's possible to run it locally for development.
You will need:
- Node.js 16.x or newer
- Pushpin - This open source GRIP proxy implementation can take the place of Fanout during development.
Preparation:
-
Configure Pushpin using
localhost:3000
by modifying theroutes
file. For example, on a default macOS installation, set the contents of/opt/homebrew/etc/pushpin/routes
:* localhost:3000
-
Install Node.js dependencies for this project.
npm install
NOTE: If you are using a version of Node.js newer than 16.x, you may see a warning during this step indicating that the version of Node.js is incompatible. This happens because this project is marked for Node.js 16.x, but this warning can safely be ignored.
To start the application:
npm run dev
This will start two processes: one to use Webpack to build the client application; and another to run the server application. Changes to source files will be monitored and the application will be rebuilt as necessary.
Now, browse to your application at http://localhost:7999/.
To run in production, you will need a Fastly Compute service with Fanout enabled.
You will also need to run the server application on an origin server that is visible from the internet.
This application is written with Glitch in mind.
NOTE: If you are using Glitch, consider boosting your app so that it doesn't go to sleep.
-
Create a new project on Glitch and import this GitHub repository. Note the public URL of your project, which typically has the domain name
https://<project-name>.glitch.me
. -
Set up the environment. In the Glitch interface, modify
.env
and set the following values:-
GRIP_URL
:https://api.fastly.com/service/<service-id>?verify-iss=fastly:<service-id>&key=<api-token>
Replace
<service-id>
with your Fastly service ID, and<api-token>
with an API token for your service that hasglobal
scope. -
GRIP_VERIFY_KEY
:base64:LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQ0tvNUExZWJ5RmNubVZWOFNFNU9uKzhHODFKeQpCalN2Y3J4NFZMZXRXQ2p1REFtcHBUbzN4TS96ejc2M0NPVENnSGZwLzZsUGRDeVlqanFjK0dNN3N3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t
(*)(*) This is a base64-encoded version of the public key published at Validating GRIP requests on the Fastly Developer Hub.
-
-
Glitch will automatically start your app by running
npm install
andnpm run start
. -
Set up the edge application on your Fastly account, and set your Glitch application as a backend for it using the name
origin
. See the edge application's README.md file for details. -
Browse to your application at the public URL of your Edge application.
This application is written with Glitch in mind, but you can alternatively use any Node.js server that is visible from the internet.
-
Clone this repository to a new directory on your Node.js server.
-
Switch to the directory and install dependencies:
npm install
-
Set the following environment variables:
-
GRIP_URL
:https://api.fastly.com/service/<service-id>?verify-iss=fastly:<service-id>&key=<api-token>
Replace
<service-id>
with your Fastly service ID, and<api-token>
with an API token for your service that hasglobal
scope. -
GRIP_VERIFY_KEY
:base64:LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQ0tvNUExZWJ5RmNubVZWOFNFNU9uKzhHODFKeQpCalN2Y3J4NFZMZXRXQ2p1REFtcHBUbzN4TS96ejc2M0NPVENnSGZwLzZsUGRDeVlqanFjK0dNN3N3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t
(*)(*) This is a base64-encoded version of the public key published at Validating GRIP requests on the Fastly Developer Hub.
-
-
Start your application:
npm run start
-
Set up the edge application on your Fastly account, and set your Node.js application as a backend for it using the name
origin
. See the edge application's README.md file for details. -
Browse to your application at the public URL of your Edge application.
It's also possible to run in production using your own instance of Pushpin. The details are beyond the scope of this document, but here are some pointers:
- Configure Pushpin to proxy to your instance of the server application
- Then set your
GRIP_URL
to point to your Pushpin instance
For more details, see Pushpin Configuration.
This program can be configured using two environment variables:
GRIP_URL
- a URL used to publish messages through a GRIP proxy. The default value ishttp://127.0.0.1:5561/
, a value that can be used in development to publish to Pushpin.GRIP_VERIFY_KEY
- (optional) a string that can be used to configure theverify-key
component ofGRIP_URL
. See Configuration of js-serve-grip for details.
The /boards/:boardId/
endpoint of the backend application checks the Accept
header of an incoming
request for text/event-stream
, and conditionally serves a stream of updates in real time, over
Server-Sent Events.
This works by using Fastly Fanout, a GRIP (Generic Realtime Intermediary
Protocol) proxy implementation. Responses for streaming requests are held
open by Fanout. Then, as updates become ready, the backend application publishes these updates through Fanout to all
connected clients.
Additionally, the ServeGrip middleware library for Express makes it easy to work with GRIP.
-
As the backend application starts,
ServeGrip
is instantiated as a singleton with information about the GRIP proxy being used, and is stored as the global variableserveGrip
. -
In
api.js
, the/boards/:boardId/
API handler checks for theAccept
header value oftext/event-stream
. If it is found, then thereq.grip
object is checked forisProxied
,needsSigned
, andisSigned
values to check that it the request came from a valid GRIP proxy.If so, then the handler uses
res.grip.startInstruct()
to issue a GRIP instruction to hold the response stream open, and to associate the response with the channel IDboard-{id}
. -
Each time the SQLite database updates an object, an event handler (
SQLite.instance.onDbUpdated()
inindex.js
) executes. If the updated object is aBoard
, then an SSE event is built and published over GRIP. The publisher is exposed by theserveGrip.getPublisher()
function.
For more details, see Realtime messaging with Fanout on Fastly's Developer Hub, the GRIP documentation, and fanout/js-serve-grip on GitHub.
The server application is written in JavaScript, for Node.js. It uses Express for the web framework with a small amount of Nunjucks templating. It stores data to a small database in SQLite. It is designed to be run in Glitch, but it can be run in any Node.js environment.
NOTE: Because it is designed to run in Glitch, it is marked as a Node.js 16.x program, as that is the newest version of Node.js supported by Glitch at the time of this writing.
This program exposes some APIs (api.js) that gives access to:
- Information about a leaderboard and its players (*)
- Information about a single player
- A method to increment the score of a player
(*) By using GRIP, this endpoint also provides updates to player information in real time, over SSE (Server-Sent Events). See Real-time updates above for more details.
It also serves the index route, served by the /
handler in index.js
, serves an HTML page that loads the
Client application, setting up the Leaderboard component with an initial list of players. The page then signs
up to real-time updates, by subscribing to the Server-Sent Events endpoint described above.
Finally, the application also serves some public files, including the browser bundle for the client application.
It has the following structure:
server/
db/
models/ - Contains the models and initialization for the DB
SQLite.js - A utility class that gives access to the DB connection
as well as emit update events
public/ - Contains static public files to be served by the Express server
views/ - Nunjucks templates for web views
api.js - API routes for routes under /boards/
index.js - Entry point, sets up the various components.
The client application is written in JavaScript, using React and
CSS Modules. It is packaged into a browser bundle using
Webpack, and is placed in the /server/public/client
directory to be served by the server
application.
This program has the following structure:
client/
components/ - React components to render a Leaderboard and player info
styles/ - Global styles for the React app
ActionsContext.js - Context object to hold actions
index.js - entry point
The entry point of this program exposes a function startLeaderboardApp()
.
This function takes an object as its argument:
rootElement - The DOM node under which to instantiate the Leaderboard component.
onIncrement - A callback function that is called whenever the '+1' button next to a
player is tapped. The function is called with the player ID as its argument.
players - An array of players in the format returned by the Player.getTopForBoard(board) function.
The function returns an object to the caller:
setPlayers - A function that can be called to cause the Leaderboard component to re-render.
It takes an updated array of players as its argument.
The server application's index template uses this mechanism to set up the Leaderboard component with an initial list of players and to sign up to real-time updates.
If you encounter any non-security-related bug or unexpected behavior, please file an issue using the bug report template.
Please see our SECURITY.md for guidance on reporting security-related issues.
MIT.