Saving data to your database

Room Service's maps & lists can be used as a collaborative "draft" you can use before saving a final copy.

The Shared Draft Pattern

In a single-player application, your user's data is stored in two places: the client and the server. On the client, you're storing working draft of the data, that might be overwritten later. And on the server, you're storing the main, authoritative copy of the data.

A user makes some changes to a document, and when they hit "save", the browser sends the current draft to the server for long-term storage.

Diagram of a client server relationship. The client is "holding" a draft copy of the data, and the server is "holding" an authoritative main copy of the data.

But in a multiplayer application, the draft if shared between users, and is therefore stored in multiple places.

A diagram of a client and server relationship, but this time there are multiple clients. The clients are collectively "holding" a single draft, split between them. The server is still holding it's authoritative copy, and there's an arrow connecting the group of clients to the server.

In this model, anyone can save, at any time. Room Service will sync drafts between users as fast as possible, but each client can save their current version to the server, at any time.

This might get weird, and that's okay.

What we're doing here is trying to force a traditionally ACID system (ie: your database) to play nicely with a BASE system (multiplayer). As you can imagine, this can get weird quickly. And in general, it's not recommended for mission critical information (billing, moon landings).

But... this is typically a fine strategy for most multiplayer applications. The draft pattern works well in practice, but not in theory. You're not going to pass a Jepsen test, but you'll ship a product folks love, on time and under budget.

To help, Room Service is built with this level of practicality in mind; whenever possible, we make the implicit edge cases explicit. But let's dive into what weirdness actually exists here, and how you can handle it.

The final version should get saved eventually.

This strategy's biggest weakness is that a user might "save" an old copy of the data to your database.

For example, say we have two users, who've both added some conflicting keys to their maps at the exact same time. Before we've sync'd over the network, each user looks like this:

1// Alice
2map.get("tree") // "oak"
3map.get("lunch") // "ramen"
4
5// Berry
6map.get("tree") // "spruce"
7map.get("lunch") // "pizza"

Eventually, they'll resolve to a single version, with winners of the conflicts chosen automatically:

1// Alice
2map.get("tree") // "oak"
3map.get("lunch") // "pizza"
4
5// Berry
6map.get("tree") // "oak"
7map.get("lunch") // "pizza"

Room Service will automatically take care of the conflict resolution and deterministically pick a winner for each key. But Room Service isn't magic; there's still a gap of time between when the sync happens and when you save.

A diagram showing a race condition, where a user, Alice, might save the data to the server before they receive an update from another user, Berry.

Save often and explain what's going on.

Treat new information as if it's changed the working draft. If a user gets updates from another user, treat it as if the information hasn't been saved yet.

1room.subscribe(map, nextMap => {
2 setSaved(false)
3
4 // ... do something with nextMap
5})

It's always best to over-communicate; show the user what's going on with UI effects.

Consider saving more frequently, both when the current user changes something, and when you receive updates. Typically it's best to use a debounce or a throttle.