DEV Community

Cover image for SvelteKit Ably: Sqvuably Real-Time Game
Rodney Lab
Rodney Lab

Posted on • Originally published at rodneylab.com

SvelteKit Ably: Sqvuably Real-Time Game

💬 Realtime Chat and Gaming Apps and Serverless WebSockets

In this SvelteKit Ably post, we talk about adding real-time events to your SvelteKit app. I have been trying out various event-driven architecture solutions recently, and have been fascinated by the offerings. WebHooks and REST hooks are the protocols I am most familiar within this space. Though, Pub-Sub and WebSockets are probably a little more common.

SvelteKit Ably - screen capture shows game in browser in form for entering a player name.

WebSockets

WebSockets enable two-way, stateful connections. HTTP connections usually involve a client requesting a document and the server responding with it, in a request-response pattern. For a real-time chat or game, clients will need to send messages or moves, live, up to the server. Similarly, the server will need to push these received updates to other participant clients; that is, without the client individually requesting them. The clients could ping repeatedly, to check for updates, over HTTP, though an open WebSocket can offer enhanced efficiency.

If you use front end tooling with Live Reload or Hot Reloading, you might already be running a WebSockets service! Here the browser opens up a WebSocket to the dev server, and typically, when an input file is changed the server closes that connection, which the client interprets as a signal to refresh the page.

Real-time

We know why WebSockets are relevant for real-time chat and gaming, but what exactly is real-time? Typically, when we talk about real-time here, we need latency (the time taken for data to pass between client and server) to be below 100 milliseconds. For fast-paced, action games players will be targeting something below 20 ms, though for chat and our little strategy game, something slower would be tolerable.

Ably: Serverless WebSockets

Ably’s platform offers real-time serverless WebSockets; ideal for adding real-time chat or multiplayer gaming to your SvelteKit app. Although you can run a SvelteKit app in node on a traditional server, the serverless pattern is currently quite common, and well-suited to SvelteKit. Alternatives to Ably which you might be familiar with are partykit, PubNub and Pusher. Personally, I have only tried Ably so far; drop a comment below if you have tried others.

We look at a simple SvelteKit strategy game in this post. I used to play the game on long car journeys, albeit with two different coloured pens and a scrap of paper! We use Ably, and the basic setup will not look too different whether you have a more sophisticated game or real-time chat in mind.

🧱 What we're Building

I created the Squavably game as a way to try out Ably’s serverless WebSocket offering. It is based on the Squares classic game. There is a tradition in the Svelte community to bolt “Sv” onto the start of any app name. Look no further than Svoast, SvHighlight and Sveriodic Table for examples. There was no option but to call the game Sqvuably 😅.

We won’t build the game from start to finish in this post instead, we’ll focus on the highlights, or should I say SvHighlights 🙄.

🍶 Server Side Ably Setup

Ably have a JavaScript SDK which will work just-fine with Svelte. Start by adding this ably package to your project:

pnpm add -D ably
Enter fullscreen mode Exit fullscreen mode

SvelteKit Ably Server Code

I based my project on an Ably Labs project, which uses older SvelteKit APIs.

import { ABLY_API_KEY } from '$env/static/private';
import { Temporal } from '@js-temporal/polyfill';
import { error as svelteKitError, fail, redirect } from '@sveltejs/kit';
import Ably from 'ably';
import type { Actions, PageServerLoad } from './$types';
import type { Types } from 'ably';

let ably: Types.RealtimePromise = new Ably.Realtime.Promise(ABLY_API_KEY);

export const actions: Actions = {
    play: async ({ cookies, request }) => {
        const form = await request.formData();
        const name = form.get('name');
        clientId = crypto.randomUUID();

        // TRUNCATED...

        // player name stored to cookie
        cookies.set('session', JSON.stringify({ clientId, name }), {
            path: '/',
            expires: new Date(Temporal.Now.plainDateTimeISO().add({ hours: 2 }).toString()),
            sameSite: 'lax',
            httpOnly: true
        });

        throw redirect(303, '/');
    }
};

export const load: PageServerLoad = async function load({ cookies, url }) {
    try {
        const session = cookies.get('session');
        if (session) {
            const { clientId, name } = JSON.parse(session);
            const token = await ably.auth.requestToken({ clientId });

            return { name, token };
        }
    } catch (error: unknown) {
        const { pathname } = url;
        const message = `Error in server load function for ${pathname}: ${error as string}`;
        throw svelteKitError(500, message);
    }
};

Enter fullscreen mode Exit fullscreen mode

I kept this logic on a +page.server rather than +page.ts, since we want to avoid sharing the API key with clients. On top, there is a little server-side state for the game, allocating players to games sessions.

Notice, the ably package has full TypeScript support, and we import Types as a named import from ably.

The actions block contains logic for logging the user in. If you are new to SvelteKit forms, take a look at the recent SvelteKit Forms post where we explore the APIs building a grammar checking app. Here, we use the front end form to get the player name and associate this information with a new SvelteKit session. These data are provided before the WebSocket is initialized, and we store the session data in an HTTP-only cookie. Then, we redirect the user browser to the “/” route on successful form submission. This will involve the page load function, now with access to the new user-associated session data.

SvelteKit Ably: Load Function

The load function is most useful once the user has filled-out the form, and we have a SvelteKit session. We initialized ably right at the top of the file:

const ably = new Ably.Realtime.Promise(ABLY_API_KEY);
Enter fullscreen mode Exit fullscreen mode

Now, in the load function, we create a new Ably auth token for the user:

const token = await ably.auth.requestToken({ clientId });

return { token };
Enter fullscreen mode Exit fullscreen mode

This is all server-side, so far. WebSocket connections start their life as regular HTTP connections, which are then upgraded. Following a regular HTTP request-response pattern, we will initialize this HTTP connection from the browser. So, the most important part here, is passing the token to the browser, allowing it to kick off the Ably connection. Returning token from the load function makes it available as a prop client-side.

I extracted a lot of the game-related detail here, and there is a link, further down, for the full code.

🧑🏽 Client-Side Ably Setup

Often, I prefer to use Svelte Actions for component related setup. Here, though, an onMount approach seemed more natural. Keen to see if you have a clean way of doing this setup in a Svelte Action!

onMount code runs after the page has been rendered. You might reach for it if you are setting up a chart for example. You will have a DOM element which the chart is drawn to. Typically, you use JavaScript code to perform that drawing task. You cannot draw to the DOM element before it exists, though! Placing the drawing code in an onMount function is a way to make sure you avoid this, as the function is only called once the DOM is rendered.

<script lang="ts">
    import app from '$lib/configuration';
    import type { Types } from 'ably';
    import { Realtime } from 'ably';
    import { onMount } from 'svelte';
    import type { PageData } from './$types';

    export let data: PageData;
    let { name, token } = data ?? {};

    const { ablyChannelName } = app;

    let channel: Types.RealtimeChannelPromise | null = null;
    $: serviceStatus = channel ? 'Connected to Ably' : 'Offline';

    onMount(async () => {
        const ably = new Realtime.Promise({
            ...token,
            authCallback: () => {
                /* add token refresh logic */
            }
        });

        await ably.connection.once('connected');

        channel = ably.channels.get(ablyChannelName);

        channel?.subscribe(({ name: messageName, data: messageData }) => {
            if (messageName === 'turn') {
        /* logic for processing message */
            }
        });
    });
</script>
Enter fullscreen mode Exit fullscreen mode
  • In line 9, we have access to the Ably token which we exported from the server load function.
  • We create a variable for an Ably channel in line 13. Channels isolate the apps messages from other apps we have. Alternatively, for a multi-player real-time game, you might have separate lobby and in-game chats. Each here could have its own channel.
  • We create the WebSocket within onMount in lines 17 – 22, using the token generated server side.

Connection Upgrade to WebSocket

Once we start the connection process, we just wait for the connection to complete. Under the hood, the HTTP connection will be upgrading to a WebSocket. Pull open your browser developer tools and track network requests. You should see a regular HTTP 200 GET request and then a 101 Switching Protocols GET request, where Ably upgrades the connection for you!

SvelteKit Ably: Web Socket upgrade in Web Developer tools.  Status shows a 101 Switching Protocols connection upgrade.

As a final step, the client needs to subscribe to the channel. Notice, we have a callback function in lines 28-32. This callback details what we want to happen when we receive a new message on the WebSocket. For a chat app, you would want to render the message for the user to read it. For the Sqvuably game, the message will be an update with the opponent’s move.

The messages can have arbitrary shape to suit your application. Here is a sample message from Sqvuably:

{
  "messageName": "turn",
  "messageData": {
    "player": "player1",
    "vertical": false,
    "row": 0,
    "column": 0
  }
}
Enter fullscreen mode Exit fullscreen mode

And an example for sending a message into the Ably channel:

<script lang="ts">
    export let channel: Types.RealtimeChannelPromise | null;

    channel.publish('turn', {
        player,
        vertical: false,
        row: rowIndex,
        column: columnIndex
    });
</script>
Enter fullscreen mode Exit fullscreen mode

❤️ A Little about the Game's Design and Svelte Implementation

The game just relies on JavaScript and HTML elements. Each grid line is an HTML button. I used CSS to get them in the right place and turn them the right colour. There are Svelte components to represent horizontal and vertical lines, and grid rows which all combine into a grid component. Buttons have descriptive text, for accessibility.

Given the structure, you might end up using a lot of prop drilling. To minimize that, I used Svelte stores for maintaining state on which player’s turn it is and whether a grid line has been claimed yet, and if so, by whom. Then, components can pick off the state they need from, avoiding prop drilling.

Typically, in a game, before all lines are covered, there is a point where it does not make sense to continue playing; that would be because neither player could claim any new squares. The Svelte stores help to simplify the logic for working out when this state has been reached.

🛠️ SvelteKit Ably: Extensions

I only really scratched the surface on what Ably itself can do, and also how you can integrate it with event-driven architecture. I have a few ideas for extensions, and am happy for you to reach out with yours: in a comment below, or however else you prefer.

The most obvious missing piece is renewing Ably tokens. These games should be quite short, so it was not an early priority, though I will look into setting it up, so I can extract the logic to other more sophisticated applications I work on.

  • Ably Presence: Presence offers an alternative method of communication on the channel. Using presence, you can, for example, automatically send “Vonda has just entered the room” style status updates in chats.

  • Upstash Redis: this is a handy service which offers serverless Redis. Redis would be fantastic for storing a leader board, which could shine a light on the most prolific players. Disclaimer: I have done writing work for Upstash in the past.

  • Kafka: Apache Kafka is an event store for real-time events. We might use it to store privacy-friendly player stats like average time to win. These data could be analysed periodically to update leader boards and other stats cached in Upstash Redis. Upstash offer serverless Kafka and Confluent is another alternative.

🙌🏽 SvelteKit Ably: Wrapping Up

In this post, we took a big-picture look at how you might set up a real-time SvelteKit Ably app. In particular, we saw:

  • what WebSockets are, and why you should consider using them
  • how Ably serverless real-time can save you time building a real-time app
  • how to set up Ably in your SvelteKit app

Please see the full repo code on the Rodney Lab GitHub repo. I do hope you have found this post useful and can use the code as a starting point for your own real-time app. I have been looking at event-driven architecture recently, using tools like ClickHouse for analytics, as well as trying out Ably. Do you want to see more content on what I am learning around these topics? Are there services or tools you are curious about and would like to see some content on? Drop a comment below or reach out on other channels.

🙏🏽 SvelteKit Ably: Feedback

If you have found this post useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on Twitter, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram. Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Search Engine Optimization among other topics. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

Top comments (0)