From e3eb34c6de57b7d8505cb4a4c9a39270df739d66 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 16 Apr 2026 22:00:58 +0700 Subject: [PATCH] fix: BITFIELD via raw REST API and improve error handling - Use Upstash REST API directly for BITFIELD (SDK builder broken in v1.37) - Add redisRaw() helper for raw command execution - Wrap setPixels in try-catch, return JSON errors instead of 500 text - Client handles non-JSON server responses gracefully - Toolbar: larger 44px touch targets, separators, better contrast --- src/client/App.svelte | 7 ++- src/client/components/DrawToolbar.svelte | 73 ++++++++++++++++++------ src/lib/canvas-storage.js | 10 ++-- src/lib/redis-client.js | 22 +++++++ src/worker.js | 10 +++- 5 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/client/App.svelte b/src/client/App.svelte index d8b9464..496d801 100644 --- a/src/client/App.svelte +++ b/src/client/App.svelte @@ -81,7 +81,12 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pixels }), }); - const data = await res.json(); + const text = await res.text(); + let data; + try { data = JSON.parse(text); } catch { + console.error('Server returned non-JSON:', res.status, text); + return; + } if (data.ok) { credits = data.credits; canvasRenderer.commitPending(); diff --git a/src/client/components/DrawToolbar.svelte b/src/client/components/DrawToolbar.svelte index aa47b6d..43cfb12 100644 --- a/src/client/components/DrawToolbar.svelte +++ b/src/client/components/DrawToolbar.svelte @@ -11,6 +11,8 @@ title="Draw mode: drag to draw, right-drag to pan">Draw +
+
@@ -18,6 +20,8 @@ title="Clear all pending pixels">Clear
+
+ @@ -26,55 +30,88 @@ diff --git a/src/lib/canvas-storage.js b/src/lib/canvas-storage.js index 6db481b..80d9582 100644 --- a/src/lib/canvas-storage.js +++ b/src/lib/canvas-storage.js @@ -1,4 +1,4 @@ -import { getRedis } from './redis-client.js'; +import { getRedis, redisRaw } from './redis-client.js'; import { CANVAS_WIDTH, TOTAL_PIXELS, BITS_PER_PIXEL, REDIS_CANVAS_KEY } from './constants.js'; /** Total bytes needed for the canvas bitfield */ @@ -53,12 +53,12 @@ export async function getFullCanvas(env) { */ export async function setPixels(env, pixels) { if (!pixels.length) return; - const redis = getRedis(env); - let chain = redis.bitfield(REDIS_CANVAS_KEY); + // Use raw REST API for BITFIELD — SDK builder API is broken in @upstash/redis 1.x + const command = ['BITFIELD', REDIS_CANVAS_KEY]; for (const { x, y, color } of pixels) { const offset = y * CANVAS_WIDTH + x; - chain = chain.set('u5', `#${offset}`, color); + command.push('SET', 'u5', `#${offset}`, String(color)); } - await chain.exec(); + await redisRaw(env, command); } diff --git a/src/lib/redis-client.js b/src/lib/redis-client.js index 4fce9bd..7af0b30 100644 --- a/src/lib/redis-client.js +++ b/src/lib/redis-client.js @@ -11,3 +11,25 @@ export function getRedis(env) { token: env.UPSTASH_REDIS_REST_TOKEN, }); } + +/** + * Execute a raw Redis command via Upstash REST API. + * Useful for commands where the SDK API is unreliable (e.g., BITFIELD). + * @param {object} env + * @param {string[]} command - Redis command as array, e.g. ['BITFIELD', 'key', 'SET', ...] + */ +export async function redisRaw(env, command) { + const res = await fetch(env.UPSTASH_REDIS_REST_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${env.UPSTASH_REDIS_REST_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(command), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Redis command failed: ${res.status} ${text}`); + } + return res.json(); +} diff --git a/src/worker.js b/src/worker.js index bc97622..55ee649 100644 --- a/src/worker.js +++ b/src/worker.js @@ -60,10 +60,14 @@ app.post('/api/place', async (c) => { return c.json({ error: 'rate_limited', remaining, retryAfter }, 429); } - // Write pixels to canvas - await setPixels(c.env, pixels); + // Write pixels to canvas + broadcast + try { + await setPixels(c.env, pixels); + } catch (err) { + console.error('Redis write failed:', err); + return c.json({ error: 'storage_failed', message: String(err) }, 500); + } - // Broadcast to all connected WebSocket clients (non-blocking) try { const roomId = c.env.CANVAS_ROOM.idFromName('main'); const room = c.env.CANVAS_ROOM.get(roomId);