惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

DEV Community

How Secure LoRa Communication Devices Work: Building the Future of Private and Long-Range Connectivity Author: Shivam Wakade | Founder, PrivSR Building a System That Automates YouTube Post-Production Building a 100% Serverless Digital Asset Packager in the Browser Game Recommended AI What is Human-In-The-Loop (HITL)? Deep Dive: React Server Components in TanStack Start Migrating off Google Analytics: Umami vs Plausible vs Fathom Building a Portfolio That Actually Demonstrates Software Engineering Async/Await in JavaScript: From Callbacks to Clean Code (2026) Benchmarking LLM Structured Outputs Angular 21 Multiselect Dropdown: A Migration-Friendly Component with Live Functional Tests ShareBox v5 — GPU transcoding, Netflix-style grid, and why I don't need Plex anymore TOML Schema is live Handling Duplicate Shopify Webhook Events (And Why You Must) Original Kubernetes Dashboard — retired upstream, upgraded to Angular 21. لماذا أسست ترينافو للتجار العرب الذين تتجاهلهم المنصات الغربية Construyendo un recomendador de películas en Python: de los datos al modelo When APIs Lie: A Lesson in Defensive Debugging Pope Leo XIV's AI Encyclical: What Builders Must Know (2026) Donna v0.3.0 HTB — MonitorsFour | Writeup The Free Tool You Trust Is the One You Should Fear the Most HTB — MonitorsFour | Writeup Fr 97. Embeddings and Vector Search: Semantic Search That Works Deep Dive: Building "Gravity Paint" - A Tactile Physics Instrument with React, Matter.js, and p5.js ABAP Unit Testing with Test Doubles and Mocking Frameworks: A Senior Architects Guide to Isolating Dependencies in SAP S/4HANA LeetCode Solution: 5. Longest Palindromic Substring kovax-react 0.8: Tailwind v4 preset, FormField adapters, ColorModeScript, and Storybook I built an AI résumé tool that refuses to lie about your experience The hat Azure Entra ID User & Role Management — Step-by-Step Practical Guide With A Simple Excercise The AI-Native Company: How a Single Founder Can Build Global Organizations Powered by AWS and an Ecosystem of Artificial Intelligences Building a Lightweight Remote MCP Knowledge Base on Cloudflare Workers Why I built Trinavo for the MENA merchants Western platforms ignore The N+1 Query That Killed Our Database, And How I Fixed It Docstrings vs Markdown Docs: What Should Developers Actually Write? Training Data Provenance: The Manifest Diff That Explains the Hash Add SVGIcons MCP to Claude Code and Find SVG Icons from Your Terminal 3 CLI Tools You Can Buy with Crypto — No KYC, No Subscriptions COSS Weekly: OpenClaw competitor NanoClaw Raises $12M, Dust Raises $40M, Sonar Acquires Gitar, and more How to know if you actually need mobile proxies (without buying any) Building Cursor for Community: A Buildathon Built on Time Pressure How we built a PII masking layer for LLM APIs — local detection, reversible tokens, one line to integrate Why MLFQ Was Way Ahead of Its Time Add Runtime Limits to Claude Agent Workflows I Built a Prompt Injection Detector with 98% Recall on Unseen Attacks. Here's Why Data Beat Architecture. 8 Vite Config Options Every Developer Should Know (Vite 8) Feature Flags That Forgot to Leave Why Trust Infrastructure Is Becoming the Hidden Layer of Donation Platforms XyPriss: Rethinking Core Performance and Zero-Trust Architecture in Modern Backends Designing Configuration for Scalable Treasure Hunts SSH Login Delays: The 10-Second Wait That Drives Us Crazy Building Production Multi-Agent Workflows in n8n: What 50 Deployments Taught Us A 3-layer memory system that gives Claude Code persistent context across sessions. Trishul SNMP Suite 2.0.1: Better MIBs, Traps, and SNMP Labs How I built a production AI SaaS as a solo developer Auto-labelling 1.2M robotics frames with VLMs: a failover story India’s Laws Were Not Built for AI — And Courts Are Filling the Gap skill-insp: A Skill That Scores Other Skills Clprolf Minimalist Messaging in the Age of AI What's actually in a good .cursorrules file? I built 10 of them — here's what I learned Building Strong Python Basics – Loops, Functions and Logic How to Choose the Right Tech Stack for Your Project I built a free multi-tab JSON editor — here's what I learned HTTP Headers Every Developer Should Know (2026) Building Cross-Platform Digital Products: Challenges and Best Practices Data Privacy in the Age of AI: How Product Teams Can Build Trust with Users What Would WordPress Look Like If It Were Designed Today? Why Backup Success Does Not Mean Database Recoverability Local AI Office Assistant That Never Sends Your Documents to the Cloud Building TaskForge: Translating Enterprise Chaos into an Open-Source Scheduler Tesla P40 in a Homelab: 24GB of Inference on a Budget Llama 4: Meta's Latest — Scout, Maverick, and the MoE Revolution George Hotz called AI code 'slop.' He's half right. Como Construir um Fluxo de Trabalho Baseado em Engenharia de Prompt e Automação We Audited Our Agent Tool-Call Traces. Half Our Eval Data Was Garbage. The Hidden Cost of Downtime: How SRE Error Budgets Protect National Economic Infrastructure Getting started with openHUMANS can be an exciting venture for developers looking to create innovative applications in the realm of human-ce Stack Overflow: A Powerful Community for Developers and Learners From Language Models to Humanoid Minds ✨ Road to Senior #2: How Computers Think in Numbers Why LLM debugging fails on fragmented repository context How to Deploy a LangGraph Agent on AWS Bedrock AgentCore An outreach kit for solo founders whose drafts can't hallucinate Open Satchel is live Amy Kwalwasser and the Growing Importance of Quantum Risk Modeling I Built ShellReq - A Native API Client for VS Code & Terminal If Microsoft and Uber can't afford AI coding, what chance do the rest of us have? MADCAP: Building a Multi-Agent Debate CLI That Argues With Itself So You Don't Have To Why most AI fails at IDOR (and how AMAS fixes it with causal reasoning) How to Audit a Laravel Codebase You've Inherited LangGraph 워크플로우 템플릿 (v34) BugBench: a developer origin story and practical guide for VS Code / Kiro users A solution to messy token systems for Next.js A NestJS reference app that proves the nest-native stack under realistic backend pressure Observability for AI Systems: Monitoring Drift, Hallucinations, and Reliability in Production I Thought “Data Analyst” Was the Whole Game… Then I Entered the Data Avengers Office 👀 Create and configure network security groups How to analyze the cost of Kafka?
How I Rebuilt an RPG Map Editor with Rust, React, and WASM
TheXper · 2026-05-26 · via DEV Community

Replacing a map editor sounds like a frontend task.

It is not.

A browser-based RPG map editor lives inside a product loop:

  1. Create a map.
  2. Open it in the editor.
  3. Paint, place assets, rename it, and change layers.
  4. Save automatically.
  5. Reopen it later and trust that nothing disappeared.

If that loop is fragile, the editor can have great brushes, nice UI, and a fast canvas, but the product still feels broken.

This post walks through how I replaced a legacy map editor with a Rust/WebAssembly editor, rebuilt the maps dashboard around the new flow, and made cloud autosave safer.

The stack:

  • Rust + Rocket for authenticated routes and map persistence
  • SQLite for map records and saved project snapshots
  • Tera templates for the product shell
  • React + Vite + WebAssembly for the editor UI
  • Rust/WASM + WebGL2 for the map editing engine

The boring parts mattered most: routing, boot state, create flow, revisions, and autosave correctness.

The actual problem

The original request was simple:

Replace the current map editor with a WASM editor.
screenshot here;

the old rpg map editor
But replacing the editor route was only the first layer.

A real D&D map maker or RPG map editor needs more than a canvas. It needs a stable workflow around the canvas. Users do not care that the editor is written in Rust or that the renderer uses WebGL2 if the New map button fails or their work does not save reliably.

So the real goal became:

Make map creation, editor boot, cloud saving, and reopening feel boring and trustworthy.

That meant fixing three areas at the same time:

  • the editor route
  • the maps dashboard
  • the save model

1. Replacing the editor route with the WASM editor

The first step was changing the authenticated editor route so that opening a map loaded the new WebAssembly editor instead of the old editor experience.

The important part was not just serving a new index.html file.

The editor needed account-aware boot data from the server:

{
  "map_id": "map_uuid",
  "title": "Dungeon Entrance",
  "revision": 12,
  "updated_at": "2026-05-25T12:00:00Z",
  "read_only": false,
  "csrf_token": "...",
  "api": {
    "load": "/api/maps/map_uuid",
    "save": "/api/maps/map_uuid"
  }
}

Enter fullscreen mode Exit fullscreen mode

That boot payload became the bridge between the product shell and the editor.

The React/Vite/WASM editor should not guess who the user is, which map is open, whether the map is editable, or where it should save. The server already knows that. The editor should receive it explicitly.

That keeps the editor focused on editing, while the product shell owns authentication, permissions, persistence, and routing.

2. The dashboard had to become part of the editor experience

Once the WASM editor route worked, the maps dashboard became the next bottleneck.

The old dashboard still behaved like a marketing page connected to an older creation wizard. But the user expectation was much simpler:

  1. Click New map.
  2. Create a map record.
  3. Redirect into the editor.

That is the core loop of a browser-based battle map maker. If this flow is not reliable, the product feels unfinished before the editor even loads.

So the dashboard was rebuilt around the editing workflow:

  • map summary cards
  • search
  • sorting
  • recent map cards
  • empty state
  • one primary create action

The dashboard stopped being a passive page and became the starting surface for map work.

3. Why the first New map fix was not enough

The first implementation used JavaScript:

await fetch('/api/maps', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify({ title: 'Untitled Map' })
});

window.location.href = `/editor/${createdMap.id}`;

Enter fullscreen mode Exit fullscreen mode

That worked in a smoke test.

Then the user reported that clicking New map did nothing.

That exposed the design flaw: a core product action depended entirely on a client-side event listener.

For something as important as creating a map, that is too fragile. JavaScript can fail because of stale assets, a bad bundle, a browser extension, a hydration issue, or a simple selector mismatch.

The fix was progressive enhancement.

The create action became a real HTML form:

<form method="post" action="/maps/new">
  <input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
  <button type="submit">New map</button>
</form>

Enter fullscreen mode Exit fullscreen mode

The server route does the critical work:

POST /maps/new
→ create map record
→ redirect to /editor/<id>

Enter fullscreen mode Exit fullscreen mode

JavaScript can still intercept the submit and use the faster API path when everything is healthy. But the fallback path is now server-owned.

That is the right tradeoff:

  • HTML handles the critical action.
  • JavaScript improves the experience.
  • The server remains the source of truth.

This is the kind of boring engineering that makes a SaaS product feel reliable.

4. Keeping the API contract small

The editor did not need a huge API surface.

The useful contract was small:

GET /api/maps/<id>
PUT /api/maps/<id>

Enter fullscreen mode Exit fullscreen mode

GET /api/maps/<id> loads the current map snapshot.

PUT /api/maps/<id> saves the new title, project JSON, and concurrency metadata.

The client sends something like:

{
  "title": "Dungeon Entrance",
  "snapshot": {
    "version": 1,
    "layers": [],
    "camera": {},
    "objects": []
  },
  "expected_revision": 12,
  "expected_updated_at": "2026-05-25T12:00:00Z"
}

Enter fullscreen mode Exit fullscreen mode

The server can then reject stale writes instead of silently overwriting newer data.

For a single-user editor, optimistic concurrency may look unnecessary. It is not. Multiple tabs, reloads, slow requests, and old local drafts can all create write conflicts.

The editor should not pretend those cases do not exist.

5. Cloud autosave has to handle real editing

The editor already had local autosave and a cloud save function.

The naive loop looked reasonable:

  1. Mark the editor dirty after a change.
  2. Wait a few seconds.
  3. Export project JSON.
  4. Send it to the server.
  5. Mark the editor saved.

The problem is that users keep editing while saves are in flight.

If an old save finishes after newer edits were made, the UI must not say everything is saved.

That is how users lose trust.

The safer model is a dirty generation counter.

Every edit increments a number:

let dirtyGeneration = 0;
let savedGeneration = 0;
let saveInFlight = false;

function markDirty() {
  dirtyGeneration += 1;
  scheduleAutosave();
}

Enter fullscreen mode Exit fullscreen mode

When a save starts, the editor captures the current generation:

async function saveToCloud() {
  if (saveInFlight) return;
  if (dirtyGeneration === savedGeneration) return;

  saveInFlight = true;
  const generationAtStart = dirtyGeneration;
  const snapshot = exportProjectJson();

  try {
    const result = await putMapSnapshot(snapshot);

    if (dirtyGeneration === generationAtStart) {
      savedGeneration = generationAtStart;
      updateRevision(result.revision, result.updated_at);
      markCloudSaved();
    } else {
      scheduleAutosaveSoon();
    }
  } catch (error) {
    keepLocalDraft();
    scheduleRetry();
  } finally {
    saveInFlight = false;
  }
}

Enter fullscreen mode Exit fullscreen mode

This protects against a common editor bug:

Save request A starts. The user makes edit B. Save request A finishes. The UI incorrectly says B is saved.

With generation tracking, the editor only clears the dirty state if no newer edits happened during the request.

6. Local drafts are a safety net, not the source of truth

Local autosave still matters.

But local storage should not become the final persistence model for an authenticated cloud editor.

The better model is:

  • local draft protects the user from crashes, reloads, and network failures
  • cloud save becomes the source of truth after a successful server write
  • server revision prevents stale overwrites
  • conflict handling decides what happens when the client is behind

That gives the user two layers of protection:

  1. Immediate local safety while editing.
  2. Durable cloud persistence once the network catches up.

For a creative tool, this matters more than it sounds. Losing a map is not a small bug. It is a trust-breaking event.

7. What changed in the product loop

Before this pass, the product had editor functionality, but the loop around it was fragile.

After the pass, the workflow became much tighter:

  1. The dashboard opens as the user's map workspace.
  2. New map always has a server-backed path.
  3. The server creates a real map record.
  4. The user lands in the Rust/WASM editor.
  5. The editor boots with map ID, API URLs, title, revision, permissions, and CSRF token.
  6. Changes save locally first.
  7. Cloud autosave persists the project snapshot with optimistic concurrency.
  8. Newer edits made during an in-flight save are not accidentally marked as saved.

That is the product loop a browser RPG map editor needs before deeper features matter.

What I would not overbuild yet

It is tempting to jump into advanced editor features immediately:

  • multiplayer editing
  • complex asset marketplaces
  • AI-generated maps
  • procedural dungeons
  • advanced sharing permissions
  • plugin systems

But those are not the first bottleneck.

The first bottleneck is trust.

For an RPG map editor, users need to believe that:

  • creating a map works
  • opening a map works
  • saving works
  • reopening works
  • their work will not disappear

Until that is true, advanced features are just decoration on top of a weak product loop.

Practical lessons

The useful lessons from this rebuild were simple:

  • Make critical actions work without JavaScript.
  • Pass explicit boot state into the editor.
  • Keep the editor API small.
  • Save title and project snapshot together.
  • Use server revisions for optimistic concurrency.
  • Track dirty generations on the client.
  • Treat local drafts as a fallback, not the final persistence layer.
  • Make saves boring.

The best editor infrastructure disappears when it works.

A user should not think about revisions, snapshots, CSRF tokens, local drafts, or save generations. They should create a dungeon map, close the tab, reopen it later, and see their work still there.

That is the real feature.

FAQ

Why use Rust and WebAssembly for a browser map editor?

Rust and WebAssembly make sense when the editor has engine-like requirements: canvas rendering, map state management, undo/redo, geometry, asset placement, and performance-sensitive operations. React can own the UI shell, while Rust/WASM owns the editing core.

Why not make the whole editor a React app?

React is good for panels, buttons, modals, inspectors, and dashboard UI. It is not the best place to put the entire editing engine. For a map editor, the canvas state, rendering model, command history, and project serialization benefit from a stricter engine layer.

Why use progressive enhancement for the New Map button?

Because creating a map is a critical action. If JavaScript fails, users should still be able to create a map and enter the editor. The enhanced JavaScript path can improve speed, but the server form path should remain reliable.

Why does autosave need a dirty generation counter?

Because users can continue editing while a save request is still running. A generation counter prevents an older save response from incorrectly marking newer edits as saved.

What is the most important part of cloud autosave?

The most important part is not the timer. It is correctness. The editor must know which edits were included in a save, which edits happened later, and whether the server accepted the write.

Final thought

Replacing the editor was the visible task.

Fixing the product loop was the real task.

For a browser-based D&D map maker, the canvas is only one part of the experience. The user also needs reliable creation, clear routing, safe autosave, and boring persistence.

That is what makes the editor feel like a product instead of a demo.

And the Result is here:

RPG Map Editor v2

RPG Map Editor v2