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