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

Published: (March 4, 2026 at 07:10 AM EST)
6 min read
Source: Dev.to

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

  1. User A clicks a button → client sends a ā€œchange stateā€ message to api‑gw‑01.
  2. The message takes ā‰ˆā€Æ80 ms to travel. During those 80 ms, the world keeps spinning.
  3. 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.
  4. 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:

  1. User clicks ā€œMove Item Leftā€.

  2. 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.

  3. 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.

  4. Server broadcasts the new, official state to all connected clients (including the originator).

  5. 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:

EventData
ACCOUNT_CREATEDinitialBalance: $0
DEPOSIT_MADEamount: $100
WITHDRAWAL_MADEamount: $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

SituationRecommended Strategy
Quick prototype / demoOptimistic UI (with clear rollback handling)
Production‑grade multiplayer appAuthoritative server + a library like Martinit‑Kit
Complex domain logic, need for audit trailsEvent 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?

SolutionImplementation SpeedUser ExperienceRobustness / Scalability
Optimistic UIFastVery responsive (but can ā€œjumpā€)Low (prone to race conditions)
Authoritative ServerMedium (frameworks help)Good (slight latency on action)High (the industry standard)
Event SourcingSlow (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:
šŸ‘‰

0 views
Back to Blog

Related posts

Read more Ā»