add project documentation and detailed README

- README: features, architecture diagram, setup guide, API reference,
  configuration table, project structure
- docs/system-architecture.md: data flow, storage design, rate limiting
- docs/code-standards.md: conventions, project layout, API format
- docs/deployment-guide.md: step-by-step CF Workers + Upstash deploy
This commit is contained in:
tiennm99 committed 2026-04-16 17:05:29 +07:00
1 parent 078ccaa70e
commit fc49de154a
4 files changed
+361 -1

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
# Code Standards
## Language & Style
- **JavaScript** (ES modules, no TypeScript)
- **Svelte 5** with runes ($state, $props, $derived, $effect)
- **kebab-case** for JS files, **PascalCase** for Svelte components
- Files under 200 lines
- No semicolons omission — use semicolons consistently
## Project Layout
```
src/
├── worker.js # Worker entry (Hono routes)
├── durable-objects/ # Cloudflare Durable Objects
├── lib/ # Shared libraries (worker + client)
├── client/ # Svelte SPA
│ ├── components/ # Svelte components (PascalCase)
│ ├── main.js # Mount entry
│ └── App.svelte # Root component
└── index.html # Vite entry
```
## Conventions
### Worker (src/worker.js, src/lib/*)
- Functions receive `env` parameter for Cloudflare bindings (Redis credentials, DO bindings)
- No global state — Workers are stateless between requests
- Use `@upstash/redis/cloudflare` (REST-based, not TCP)
- Bitfield operations use builder pattern: `redis.bitfield(key).set().exec()`
### Client (src/client/*)
- Svelte 5 runes only (`$state`, `$props`, `$derived`, `$effect`)
- No stores — pass state via props and callbacks
- Canvas rendering is imperative (OffscreenCanvas + putImageData)
- Touch and mouse handlers coexist on the same canvas element
### Shared (src/lib/constants.js, src/lib/canvas-decoder.js)
- Imported by both worker and client
- Vite tree-shakes worker-only code from client bundle
- Constants are the single source of truth for canvas dimensions, colors, limits
## API Response Format
Success: `{ ok: true, credits: N }`
Error: `{ error: "error_code", ...details }` with appropriate HTTP status
+74
View File
@@ -0,0 +1,74 @@
# Deployment Guide
## Prerequisites
- Node.js 18+
- Cloudflare account (free tier)
- Upstash Redis database (free tier)
- `wrangler` CLI (installed as dev dependency)
## Step 1: Create Upstash Redis Database
1. Go to [console.upstash.com](https://console.upstash.com/)
2. Create a new Redis database
3. Choose a region close to your users
4. Copy the **REST URL** and **REST Token**
## Step 2: Configure Secrets
```bash
# Set secrets in Cloudflare (not in code)
npx wrangler secret put UPSTASH_REDIS_REST_URL
npx wrangler secret put UPSTASH_REDIS_REST_TOKEN
```
For local development, create `.dev.vars`:
```
UPSTASH_REDIS_REST_URL=https://your-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token
```
## Step 3: Deploy
```bash
npm run deploy
```
This runs `vite build` (compiles Svelte → dist/) then `wrangler deploy` (uploads Worker + static assets).
## Step 4: Verify
1. Visit your Worker URL (e.g., `https://rplace.your-subdomain.workers.dev`)
2. Canvas should load (empty/dark on first visit)
3. Select a color, click to place a pixel
4. Open a second browser tab — pixel should appear via WebSocket
## Custom Domain
```bash
# Add a custom domain via Cloudflare dashboard or:
npx wrangler domains add rplace.yourdomain.com
```
## Monitoring
- **Cloudflare dashboard**: Worker analytics, request logs, DO metrics
- **Upstash console**: Redis command count, memory usage, latency
## Cost Estimates (Free Tier)
| Resource | Free Limit | rplace Usage |
|---|---|---|
| CF Workers | 100K requests/day | Canvas reads + pixel placements |
| CF Durable Objects | Free with Workers | WebSocket connections |
| Upstash Redis | 10K commands/day | BITFIELD reads/writes + rate limiting |
For hobby traffic (< few hundred users/day), free tiers are sufficient. Upstash pay-as-you-go ($0.2/100K commands) is the first thing to hit limits.
## Troubleshooting
- **Canvas loads empty**: Check Upstash credentials in secrets
- **Pixels don't persist**: Verify BITFIELD support — test with `redis-cli BITFIELD canvas SET u5 #0 1`
- **WebSocket not connecting**: Ensure Durable Object migration ran (check `wrangler.json` migrations)
- **Rate limiting not working**: Verify `redis.eval()` works on your Upstash tier (Lua scripting)
+81
View File
@@ -0,0 +1,81 @@
# System Architecture
## Overview
rplace is a collaborative pixel canvas deployed as a single Cloudflare Worker. The frontend (Svelte SPA) is served as static assets, the API (Hono) handles pixel operations, and a Durable Object manages WebSocket broadcasting.
## Data Flow
### Pixel Placement
```
1. User clicks canvas → optimistic render + credit deduction
2. POST /api/place { pixels: [{x, y, color}] }
3. Worker validates input (bounds, types, batch size ≤ 32)
4. Worker checks credits via Lua script (atomic check-and-deduct)
5. Worker writes pixels via Redis BITFIELD (atomic batch)
6. Worker sends pixels to Durable Object /broadcast
7. Durable Object fans out to all WebSocket clients
8. Response: { ok: true, credits: N }
```
### Canvas Loading
```
1. Client fetches GET /api/canvas
2. Worker reads Redis key via GETRANGE → raw binary
3. Client receives ~2.5MB (5-bit packed pixels)
4. Client decodes 5-bit values → color indices → RGBA ImageData
5. Renders onto HTML5 Canvas with OffscreenCanvas
```
### Real-time Updates
```
1. Client connects WS /api/ws
2. Worker upgrades to Durable Object WebSocket
3. DO stores connection in memory Set
4. On pixel placement → DO broadcasts JSON to all connections
5. Client updates local ImageData + re-renders
6. On disconnect → auto-reconnect with exponential backoff (1s→30s)
```
## Storage
### Redis BITFIELD (Canvas)
- Key: `canvas`
- Encoding: 5 bits per pixel (u5), 32 colors
- Size: `2048 * 2048 * 5 / 8 = 2,621,440 bytes` (~2.5MB)
- Offset: `y * CANVAS_WIDTH + x`
- Atomic batch writes: single BITFIELD command with chained .set() calls
### Redis HASH (Credits)
- Key pattern: `credits:{userId}`
- Fields: `lu` (last update, unix seconds), `cr` (current credits)
- TTL: 24 hours (auto-expire inactive users)
- Accessed via Lua script for atomic check-and-deduct
## Rate Limiting
Token bucket algorithm implemented as a Lua script:
```
On placement request:
1. Read stored credits + lastUpdate from HASH
2. Calculate accrued = stored + floor(elapsed_seconds * regen_rate)
3. Cap at MAX_CREDITS (256)
4. If accrued < requested → reject (429)
5. Else → deduct, update HASH, return remaining
```
New users start with full credits (256). Anonymous identity via CF-Connecting-IP hash.
## Security
- **Rate limiting**: Atomic Lua script prevents race conditions
- **Identity**: CF-Connecting-IP (set by Cloudflare, unspoofable)
- **Input validation**: Bounds checking, type checking, integer validation on all pixel data
- **Batch cap**: Max 32 pixels per request to limit burst damage
- **DO isolation**: /broadcast route only reachable via DO stub, not externally