DEV Community

Cover image for Solved: Martinit-Kit: Typescript runtime that syncs state across users for your multiplayer app/game
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Martinit-Kit: Typescript runtime that syncs state across users for your multiplayer app/game

šŸš€ Executive Summary

TL;DR: Multiplayer applications frequently encounter real-time state synchronization issues due to network latency and race conditions, causing client desynchronization. The article introduces Martinit-Kit as a Typescript runtime that facilitates robust state synchronization, primarily through the authoritative server model, establishing a single source of truth for all connected users.

šŸŽÆ Key Takeaways

  • Network latency is the fundamental cause of state desynchronization in multiplayer applications, leading to race conditions when multiple users attempt to modify the same state concurrently.
  • The Authoritative Server model is the industry standard for robust state synchronization, where the server acts as the sole source of truth, validating intents and broadcasting official state changes to all clients.
  • Optimistic UI enhances perceived performance by updating the client’s screen instantly, but it carries the risk of jarring rollbacks if the server rejects the change, making it best suited for low-conflict actions.
  • Event Sourcing provides an immutable log of all state-changing actions, offering perfect audit trails and the ability to replay history, but it represents a significant architectural shift for complex systems.

Struggling with real-time state synchronization in your multiplayer Typescript app? I’m breaking down why it’s so painful and offering three battle-tested solutions, from quick client-side tricks to robust server-side architectures.

That Desync Feeling: Fixing Multiplayer State Nightmares

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. That’s the day I learned that shared state isn’t a feature; it’s a distributed systems boss battle, and you’d better come prepared.

So, Why Does This Keep Happening? The Root of the Problem.

You see a Reddit thread about a cool new library like ā€˜Martinit-Kit’ and think it’s a silver bullet. It might be, but to use it effectively, you need to understand the beast you’re fighting. The core issue isn’t your code; it’s physics. The internet isn’t instant.

Here’s the breakdown:

  • User A clicks a button. Their client sends a ā€œchange stateā€ message to your server, api-gw-01.
  • That message takes 80ms to travel. During those 80ms, 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 gets two conflicting instructions. Who wins? Does the last one in overwrite the first? What if the first one was more important?

This is a classic race condition. Without a single, undisputed source of truth and a clear set of rules for applying changes, your clients will inevitably drift apart, leading to chaos, confusion, and disappearing components during VP demos.

The Battle Plan: Three Ways to Slay the Desync Dragon

Over the years, we’ve developed a few standard plays for this problem at TechResolve. They range from ā€œget it working for the presentation tomorrowā€ to ā€œre-architect for a million concurrent users.ā€

Solution 1: The Quick Fix – ā€œOptimistic UIā€

This is the ā€œfake it ’til you make itā€ approach, and honestly, it’s great for perceived performance. The idea is to update the user’s own screen immediately, assuming the server will agree. You send the update to the server in the background and hope for the best.

// Super simplified pseudo-code
function handleMoveButtonClick(itemId, newPosition) {
  // 1. Update our own UI instantly. Feels fast!
  const previousPosition = updateLocalItemPosition(itemId, newPosition);

  // 2. Now, tell the server what we did.
  api.sendItemMove(itemId, newPosition)
    .catch(error => {
      // 3. Whoops, 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.");
    });
}
Enter fullscreen mode Exit fullscreen mode

Pro Tip: This feels magically fast to the user, but be warned. 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. This can be jarring, so use it for low-conflict actions.

Solution 2: The Permanent Fix – The ā€œAuthoritative Serverā€ Model

This is the grown-up solution and the one you should build toward. In this model, the client is ā€œdumb.ā€ It doesn’t decide the state; it only sends intents to the server. The server is the one and only source of truth.

The flow looks like this:

  1. User clicks ā€œMove Item Left.ā€
  2. The client sends a message like { action: 'MOVE_INTENT', itemId: 'abc-123', direction: 'left' }. The UI might show a spinner, but it does not move the item yet.
  3. Your server (e.g., game-state-worker-03) receives the intent. It validates it, checks for conflicts, and updates the canonical state in its memory or in a fast cache like Redis.
  4. The server then broadcasts the new, official state to all connected clients (including the one that sent the intent).
  5. All clients receive the new state and render it. Everyone is perfectly in sync because they are all just mirrors of the server.

Frameworks like the Martinit-Kit mentioned in that thread 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, which is the heart of the matter.

Solution 3: The ā€˜Nuclear’ Option – Event Sourcing

Sometimes, the state logic is so complex that just storing the ā€œcurrent stateā€ isn’t enough. What if you need to know how it got that way? Enter Event Sourcing. Instead of storing the final result, you store every single action (event) that ever happened in an immutable log.

Imagine a bank account. Instead of a database row that says balance: $50, you store a log:

  • ACCOUNT_CREATED, initialBalance: $0
  • DEPOSIT_MADE, amount: $100
  • WITHDRAWAL_MADE, amount: $50

The current balance ($50) is calculated by replaying these events. This sounds like more work, and it is. But the benefits are immense for complex systems: perfect audit trails, the ability to debug by ā€œreplayingā€ history to find a bug, and the power to create different ā€œviewsā€ of the state from the same event log.

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.

Making the Call

So, which one is right for you? I made a quick table to help you decide.

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 at 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, well, that’s a good problem to have, and it might be time to read up on Event Sourcing. Now go build something cool, and try not to let the VPs break it.


Darian Vance

šŸ‘‰ Read the original article on TechResolve.blog


ā˜• Support my work

If this article helped you, you can buy me a coffee:

šŸ‘‰ https://buymeacoffee.com/darianvance

Top comments (0)