mirror of
https://github.com/tiennm99/rplace.git
synced 2026-09-10 06:21:01 +00:00
fix(canvas): chunk GETRANGE to stay under Upstash 10MB response cap
Single GETRANGE of the 16 MiB canvas base64-encodes to ~22 MB, exceeding Upstash's 10 MB per-request limit on the Free plan and causing 500s on /api/canvas. Split into 4x4 MiB parallel ranges (~5.3 MB base64 each).
This commit is contained in:
+32
-17
@@ -3,34 +3,49 @@ import { CANVAS_WIDTH, TOTAL_PIXELS, REDIS_CANVAS_KEY } from './constants.js';
|
||||
|
||||
const CANVAS_BYTES = TOTAL_PIXELS;
|
||||
|
||||
// Upstash REST returns binary as base64 (~4/3 overhead) and caps responses at
|
||||
// 10 MB on the Free plan. 4 MiB raw → ~5.33 MB base64, safely under the limit,
|
||||
// and divides a 16 MiB canvas into exactly 4 chunks.
|
||||
const CHUNK_BYTES = 4 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Get the full canvas as a Uint8Array of raw bytes.
|
||||
* Uses raw REST API to avoid SDK binary encoding corruption.
|
||||
* Returns a zero-filled buffer if canvas doesn't exist yet.
|
||||
* Fetches in parallel chunks to stay under Upstash's per-request size cap.
|
||||
* Returns a zero-filled buffer if canvas doesn't exist yet; truncated chunks
|
||||
* are zero-padded (GETRANGE clamps past-end reads to "" by default).
|
||||
* @param {object} env
|
||||
* @returns {Promise<Uint8Array>}
|
||||
*/
|
||||
export async function getFullCanvas(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)]);
|
||||
|
||||
if (!base64 || base64.length === 0) {
|
||||
return new Uint8Array(CANVAS_BYTES);
|
||||
const ranges = [];
|
||||
for (let start = 0; start < CANVAS_BYTES; start += CHUNK_BYTES) {
|
||||
const end = Math.min(start + CHUNK_BYTES, CANVAS_BYTES) - 1;
|
||||
ranges.push([start, end]);
|
||||
}
|
||||
|
||||
const raw = atob(base64);
|
||||
const bytes = new Uint8Array(raw.length);
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
bytes[i] = raw.charCodeAt(i);
|
||||
const results = await Promise.all(
|
||||
ranges.map(([start, end]) =>
|
||||
redisRawBinary(env, ['GETRANGE', REDIS_CANVAS_KEY, String(start), String(end)])
|
||||
)
|
||||
);
|
||||
|
||||
const out = new Uint8Array(CANVAS_BYTES);
|
||||
let totalRead = 0;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const base64 = results[i];
|
||||
if (!base64) continue;
|
||||
const raw = atob(base64);
|
||||
const offset = ranges[i][0];
|
||||
for (let j = 0; j < raw.length; j++) {
|
||||
out[offset + j] = raw.charCodeAt(j);
|
||||
}
|
||||
totalRead += raw.length;
|
||||
}
|
||||
|
||||
if (bytes.length < CANVAS_BYTES) {
|
||||
console.warn(`Canvas read truncated: got ${bytes.length} bytes, expected ${CANVAS_BYTES}; zero-padding tail`);
|
||||
const padded = new Uint8Array(CANVAS_BYTES);
|
||||
padded.set(bytes);
|
||||
return padded;
|
||||
if (totalRead > 0 && totalRead < CANVAS_BYTES) {
|
||||
console.warn(`Canvas read short: got ${totalRead} bytes, expected ${CANVAS_BYTES}; zero-padding tail`);
|
||||
}
|
||||
return bytes;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user