Solved: Martinit-Kit: Typescript runtime that syncs state across users for your multiplayer app/game
Source: Dev.to
Executive Summary
TL;DR: Multiplayer applications frequently encounter realātime stateāsynchronization issues due to network latency and race conditions, causing client desynchronization. This article introduces MartinitāKit, a TypeScript runtime that facilitates robust state synchronization, primarily through the authoritative server model, establishing a single source of truth for all connected users.
The Problem
- Network latency is the fundamental cause of state desynchronization in multiplayer apps.
- Latency creates race conditions when multiple users try to modify the same state concurrently.
Authoritative Server Model
- Industryāstandard for robust state synchronization.
- The server acts as the sole source of truth, validates intents, and broadcasts official state changes to all clients.
Optimistic UI
- Enhances perceived performance by updating the clientās screen instantly.
- Risk: Jarring rollbacks if the server rejects the change. Best suited for lowāconflict actions.
Event Sourcing
- Provides an immutable log of all stateāchanging actions.
- Offers perfect audit trails and the ability to replay history.
- Represents a significant architectural shift for complex systems.
Why Itās Painful
āIāll never forget the demo for ProjectāÆChimera. We were showing our new collaborative design tool to the VPs. Everything was smooth until two execs tried to move the same component at the same time. On one screen it snapped left; on the other it went right. Then, for a glorious ten seconds, it flickered between both spots before vanishing entirely into the digital ether. The silence in that room was⦠deafening.ā
Shared state isnāt a feature; itās a distributedāsystems boss battle.
A Simple Walkāthrough
- UserāÆA clicks a button ā client sends a āchange stateā message to
apiāgwā01. - The message takes āāÆ80āÆms to travel. During those 80āÆms, the world keeps spinning.
- UserāÆB, who hasnāt received UserāÆAās update yet, clicks a different button that modifies the same piece of state. Their message is now on its way.
- The server receives two conflicting instructions.
- Who wins?
- Does the last one overwrite the first?
- What if the first one was more important?
Without a single, undisputed source of truth and clear conflictāresolution rules, clients will inevitably drift apart, leading to chaos, confusion, and disappearing components during VP demos.
Common Solutions (From āFakeāItāTillāYouāMakeāItā to FullāScale Architecture)
1. Optimistic UI ā āFake it ātil you make itā
Great for perceived performance. The idea is to update the userās own screen immediately, assuming the server will agree.
// Superāsimplified pseudoācode
function handleMoveButtonClick(itemId: string, newPosition: Position) {
// 1ļøā£ Update our own UI instantly. Feels fast!
const previousPosition = updateLocalItemPosition(itemId, newPosition);
// 2ļøā£ Tell the server what we did.
api.sendItemMove(itemId, newPosition)
.catch(error => {
// 3ļøā£ Oops ā server rejected it! Roll back our optimistic change.
console.error("Move rejected by server:", error);
updateLocalItemPosition(itemId, previousPosition); // Jumps back!
showErrorToast("Couldn't move the item.");
});
}
Pro Tip: This feels magically fast to the user, but if the server rejects the change (e.g., due to a permissions issue or conflict), the UI element will ājumpā back to its original position. Use it only for lowāconflict actions.
2. Authoritative Server ā The āGrownāupā Solution
In this model the client is dumb: it never decides the final state, it only sends intents to the server. The server is the only source of truth.
Flow:
-
User clicks āMove Item Leftā.
-
Client sends a message, e.g.:
{ "action": "MOVE_INTENT", "itemId": "abc-123", "direction": "left" }The UI may show a spinner, but it does not move the item yet.
-
Server (e.g.,
gameāstateāworkerā03) receives the intent, validates it, checks for conflicts, and updates the canonical state in memory or a fast cache like Redis. -
Server broadcasts the new, official state to all connected clients (including the originator).
-
All clients receive the new state and render it. Everyone stays perfectly in sync because they are mirrors of the server.
Frameworks like MartinitāKit are built specifically to make this pattern easier. They handle the boilerplate of WebSockets, state broadcasting, and reconciliation, letting you focus on the serverāside logicāthe heart of the matter.
3. Event Sourcing ā When āCurrent Stateā Isnāt Enough
Sometimes the state logic is so complex that storing only the current state isnāt sufficient. You need to know how it got there.
Enter Event Sourcing. Instead of storing the final result, you store every single action (event) that ever happened in an immutable log.
Example ā Bank Account:
| Event | Data |
|---|---|
ACCOUNT_CREATED | initialBalance: $0 |
DEPOSIT_MADE | amount: $100 |
WITHDRAWAL_MADE | amount: $50 |
The current balance ($50) is calculated by replaying these events. This approach gives you perfect auditability and the ability to reconstruct any past state, at the cost of added architectural complexity.
Choosing the Right Approach
| Situation | Recommended Strategy |
|---|---|
| Quick prototype / demo | Optimistic UI (with clear rollback handling) |
| Productionāgrade multiplayer app | Authoritative server + a library like MartinitāKit |
| Complex domain logic, need for audit trails | Event Sourcing (often combined with an authoritative server) |
Final Thoughts
- Latency ā Instantaneous ā The internet isnāt instant; design for the delay.
- Single Source of Truth ā Prevents drift and race conditions.
- Clear Conflict Rules ā Decide upfront how to resolve competing intents.
By understanding the underlying physics of distributed systems and choosing the appropriate architecture, you can turn the ādistributedāsystems boss battleā into a manageable, predictable process. Happy syncing!
Warning: Do not take this path lightly. This is a fundamental architectural shift. It requires a different way of thinking and tooling (like Kafka or a dedicated event store). Itās incredibly powerful for the right problem, but itās not a quick fix for a simple chat app.
So, which one is right for you?
| Solution | Implementation Speed | User Experience | Robustness / Scalability |
|---|---|---|---|
| Optimistic UI | Fast | Very responsive (but can ājumpā) | Low (prone to race conditions) |
| Authoritative Server | Medium (frameworks help) | Good (slight latency on action) | High (the industry standard) |
| Event Sourcing | Slow (major undertaking) | Good (same as authoritative) | Very High (complex but powerful) |
My advice?
Start with the Authoritative Server model. Itās the sweet spot of reliability and implementation effort. Look for tools that get you there faster. If your app feels sluggish, you can sprinkle in some Optimistic UI for nonācritical actions. And if your app becomes the next Google Docs, thatās a good problem to haveāit might be time to read up on Event Sourcing.
Now go build something cool, and try not to let the VPs break it.
š Read the original article on TechResolve.blog
ā Support my work
If this article helped you, you can buy me a coffee:
š