diff --git a/src/lib/canvas-storage.js b/src/lib/canvas-storage.js index 80d9582..c63b201 100644 --- a/src/lib/canvas-storage.js +++ b/src/lib/canvas-storage.js @@ -1,4 +1,4 @@ -import { getRedis, redisRaw } from './redis-client.js'; +import { redisRaw, redisRawBinary } 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 */ @@ -6,55 +6,42 @@ const CANVAS_BYTES = Math.ceil((TOTAL_PIXELS * BITS_PER_PIXEL) / 8); /** * Get the full canvas as a Uint8Array of raw bytes. - * Uses GETRANGE to fetch raw binary reliably (avoids encoding issues with GET). + * Uses raw REST API to avoid SDK binary encoding corruption. * Returns a zero-filled buffer if canvas doesn't exist yet. * @param {object} env * @returns {Promise} */ export async function getFullCanvas(env) { - const redis = getRedis(env); + // Use base64 encoding for binary-safe transport of BITFIELD data + const base64 = await redisRawBinary(env, ['GETRANGE', REDIS_CANVAS_KEY, '0', String(CANVAS_BYTES - 1)]); - // Use GETRANGE to fetch the full string as raw bytes - // This is more reliable than GET for binary data written by BITFIELD - const data = await redis.getrange(REDIS_CANVAS_KEY, 0, CANVAS_BYTES - 1); - if (!data || data.length === 0) { + if (!base64 || base64.length === 0) { return new Uint8Array(CANVAS_BYTES); } - // Upstash REST may return base64 or raw string for binary data - if (typeof data === 'string') { - let raw; - try { - raw = atob(data); - } catch { - raw = data; // Already a raw string - } - const bytes = new Uint8Array(raw.length); - for (let i = 0; i < raw.length; i++) { - bytes[i] = raw.charCodeAt(i); - } - // Pad to full canvas size if shorter - if (bytes.length < CANVAS_BYTES) { - const padded = new Uint8Array(CANVAS_BYTES); - padded.set(bytes); - return padded; - } - return bytes; + const raw = atob(base64); + const bytes = new Uint8Array(raw.length); + for (let i = 0; i < raw.length; i++) { + bytes[i] = raw.charCodeAt(i); } - return new Uint8Array(CANVAS_BYTES); + if (bytes.length < CANVAS_BYTES) { + const padded = new Uint8Array(CANVAS_BYTES); + padded.set(bytes); + return padded; + } + return bytes; } /** * Set multiple pixels in a single atomic BITFIELD command. - * Uses Upstash's builder pattern: redis.bitfield(key).set().set().exec() + * Uses raw REST API — SDK bitfield builder is broken in @upstash/redis 1.x. * @param {object} env * @param {Array<{x: number, y: number, color: number}>} pixels */ export async function setPixels(env, pixels) { if (!pixels.length) return; - // 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; diff --git a/src/lib/redis-client.js b/src/lib/redis-client.js index 7af0b30..8a19de8 100644 --- a/src/lib/redis-client.js +++ b/src/lib/redis-client.js @@ -33,3 +33,26 @@ export async function redisRaw(env, command) { } return res.json(); } + +/** + * Execute a Redis command via Upstash path-based REST API with base64 response. + * Uses Upstash-Encoding: base64 for binary-safe response transport. + * @param {object} env + * @param {string[]} command - Redis command as array, e.g. ['GETRANGE', 'key', '0', '100'] + * @returns {Promise} base64-encoded result string + */ +export async function redisRawBinary(env, command) { + const path = command.map((arg) => encodeURIComponent(String(arg))).join('/'); + const res = await fetch(`${env.UPSTASH_REDIS_REST_URL}/${path}`, { + headers: { + Authorization: `Bearer ${env.UPSTASH_REDIS_REST_TOKEN}`, + 'Upstash-Encoding': 'base64', + }, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Redis command failed: ${res.status} ${text}`); + } + const { result } = await res.json(); + return result; // base64-encoded string +}