mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-11 03:13:48 +00:00
fix: binary-safe canvas read via Upstash base64 encoding
Use path-based REST API with Upstash-Encoding: base64 header for GETRANGE to prevent binary data corruption through JSON text encoding. Verified all 32 colors round-trip correctly.
This commit is contained in:
1 parent
e3eb34c6de
commit
b35769cc73
2 files changed
+39
-29
No files matched your search
+16
-29
@@ -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';
|
import { CANVAS_WIDTH, TOTAL_PIXELS, BITS_PER_PIXEL, REDIS_CANVAS_KEY } from './constants.js';
|
||||||
|
|
||||||
/** Total bytes needed for the canvas bitfield */
|
/** 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.
|
* 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.
|
* Returns a zero-filled buffer if canvas doesn't exist yet.
|
||||||
* @param {object} env
|
* @param {object} env
|
||||||
* @returns {Promise<Uint8Array>}
|
* @returns {Promise<Uint8Array>}
|
||||||
*/
|
*/
|
||||||
export async function getFullCanvas(env) {
|
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
|
if (!base64 || base64.length === 0) {
|
||||||
// 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) {
|
|
||||||
return new Uint8Array(CANVAS_BYTES);
|
return new Uint8Array(CANVAS_BYTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstash REST may return base64 or raw string for binary data
|
const raw = atob(base64);
|
||||||
if (typeof data === 'string') {
|
const bytes = new Uint8Array(raw.length);
|
||||||
let raw;
|
for (let i = 0; i < raw.length; i++) {
|
||||||
try {
|
bytes[i] = raw.charCodeAt(i);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
* 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 {object} env
|
||||||
* @param {Array<{x: number, y: number, color: number}>} pixels
|
* @param {Array<{x: number, y: number, color: number}>} pixels
|
||||||
*/
|
*/
|
||||||
export async function setPixels(env, pixels) {
|
export async function setPixels(env, pixels) {
|
||||||
if (!pixels.length) return;
|
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];
|
const command = ['BITFIELD', REDIS_CANVAS_KEY];
|
||||||
for (const { x, y, color } of pixels) {
|
for (const { x, y, color } of pixels) {
|
||||||
const offset = y * CANVAS_WIDTH + x;
|
const offset = y * CANVAS_WIDTH + x;
|
||||||
|
|||||||
@@ -33,3 +33,26 @@ export async function redisRaw(env, command) {
|
|||||||
}
|
}
|
||||||
return res.json();
|
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<string|null>} 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
|
||||||
|
}
|
||||||
Reference in new issue
Block a user