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

推荐订阅源

GbyAI
GbyAI
V
Visual Studio Blog
美团技术团队
L
LINUX DO - 最新话题
Last Week in AI
Last Week in AI
雷峰网
雷峰网
博客园_首页
腾讯CDC
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
J
Java Code Geeks
W
WeLiveSecurity
Apple Machine Learning Research
Apple Machine Learning Research
Spread Privacy
Spread Privacy
博客园 - 聂微东
量子位
Recent Announcements
Recent Announcements
S
Schneier on Security
O
OpenAI News
PCI Perspectives
PCI Perspectives
H
Heimdal Security Blog
T
Tailwind CSS Blog
S
Security Affairs
Y
Y Combinator Blog
P
Privacy International News Feed
Hacker News: Ask HN
Hacker News: Ask HN
Stack Overflow Blog
Stack Overflow Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
U
Unit 42
Webroot Blog
Webroot Blog
Vercel News
Vercel News
Simon Willison's Weblog
Simon Willison's Weblog
B
Blog RSS Feed
S
Secure Thoughts
Microsoft Azure Blog
Microsoft Azure Blog
N
Netflix TechBlog - Medium
V
V2EX
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
D
DataBreaches.Net
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
L
LangChain Blog
S
Security @ Cisco Blogs
The Hacker News
The Hacker News
The GitHub Blog
The GitHub Blog
C
CERT Recently Published Vulnerability Notes

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
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"
  }
}

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}`;

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>

The server route does the critical work:

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

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>

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"
}

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();
}

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;
  }
}

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