mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-14 22:17:37 +00:00
fix(canvas): code-review fixes + sync user-facing docs to DO storage
Fixes from code review of canvas-on-do migration (commit c3f7c02):
- worker.js /api/ws: rewrite request URL to '/ws' so the DO pathname
switch dispatches correctly. The original c.req.raw kept '/api/ws'
which the DO never matched → 404 on every WS upgrade.
- migrate-from-upstash.js pickSampleOffsets: use TOTAL_PIXELS - 1 for the
last byte instead of CANVAS_WIDTH * CANVAS_WIDTH (only correct when
the canvas is square; constants explicitly invite non-square).
- chunk-storage.js writePixels: clarify atomicity comment — the loop is
atomic *because it has no awaits*, not because of any implicit DO
transaction. Added guidance for future maintainers.
- cooldown-store.js tryAcquire: GC sweep wrapped in try/catch so a
transient failure can't drop the user's allowed: true response.
Docs:
- README.md: drop Upstash from tech stack, redraw architecture,
document new project layout (durable-objects/lib, admin/), add
CHUNK_BYTES to configuration table.
- docs/system-architecture.md: full rewrite for DO-storage data flow,
document SQLite schema, race-safe rate-limit pattern, free-tier table.
- docs/deployment-guide.md: drop Upstash setup, add optional one-shot
migration runbook, update free-tier table to actual May 2026 limits.
Tests: 112 pass, 6 skipped (pending Phase 4 rewrite via
@cloudflare/vitest-pool-workers). Bundle dry-run clean.
Local wrangler dev smoke test was attempted but the sandboxed env
hangs HTTP requests at the workerd layer (TCP connects, no response).
Routing fix verified by code inspection; user must verify in their
own dev or production.
This commit is contained in:
1 parent
cdf2295ef6
commit
b890dfb3b7
7 files changed
+271
-137
No files matched your search
@@ -80,8 +80,8 @@ export async function migrateFromUpstash(env, roomStub, { force = false } = {})
|
||||
function pickSampleOffsets(srcBytes) {
|
||||
const offsets = new Set([
|
||||
0, // (0, 0)
|
||||
CANVAS_WIDTH - 1, // top-right
|
||||
(CANVAS_WIDTH * CANVAS_WIDTH) - 1, // bottom-right (square canvas)
|
||||
CANVAS_WIDTH - 1, // first-row right edge
|
||||
TOTAL_PIXELS - 1, // last byte
|
||||
Math.floor(TOTAL_PIXELS / 2), // middle
|
||||
Math.floor(TOTAL_PIXELS / 2) + CANVAS_WIDTH + 1, // off-middle
|
||||
]);
|
||||
|
||||
@@ -81,8 +81,10 @@ export function writePixels(sql, pixels) {
|
||||
|
||||
// For each touched chunk: read current bytes (or zero-fill), apply edits,
|
||||
// write back. INSERT OR REPLACE upserts the row.
|
||||
// Note: CF DO transactions are implicit per-fetch handler invocation —
|
||||
// multiple sql.exec calls within the same handler are atomic.
|
||||
// Atomicity: this loop is fully synchronous (no `await`) and the DO is
|
||||
// single-threaded, so the chunk updates are atomic with respect to other
|
||||
// requests. If anyone adds an `await` inside this loop, wrap the whole
|
||||
// block in `state.storage.transactionSync(() => { ... })` to preserve it.
|
||||
for (const [chunkId, edits] of groups) {
|
||||
const buf = readChunk(sql, chunkId);
|
||||
// readChunk returns a fresh Uint8Array (or wraps a buffer); writes must
|
||||
|
||||
@@ -33,7 +33,9 @@ export function tryAcquire(sql, userId, now = Date.now()) {
|
||||
// Drain the cursor so rowsWritten is finalized.
|
||||
updateCursor.toArray();
|
||||
if (updateCursor.rowsWritten > 0) {
|
||||
if (Math.random() < GC_SAMPLE_RATE) gc(sql, now);
|
||||
if (Math.random() < GC_SAMPLE_RATE) {
|
||||
try { gc(sql, now); } catch { /* GC is best-effort */ }
|
||||
}
|
||||
return { allowed: true, retryAfter: 0 };
|
||||
}
|
||||
|
||||
@@ -45,7 +47,9 @@ export function tryAcquire(sql, userId, now = Date.now()) {
|
||||
userId,
|
||||
expiresAt,
|
||||
);
|
||||
if (Math.random() < GC_SAMPLE_RATE) gc(sql, now);
|
||||
if (Math.random() < GC_SAMPLE_RATE) {
|
||||
try { gc(sql, now); } catch { /* GC is best-effort */ }
|
||||
}
|
||||
return { allowed: true, retryAfter: 0 };
|
||||
} catch {
|
||||
return { allowed: false, retryAfter: REQUEST_COOLDOWN_SEC };
|
||||
|
||||
+4
-2
@@ -62,13 +62,15 @@ app.post('/api/place', async (c) => {
|
||||
});
|
||||
});
|
||||
|
||||
/** GET /api/ws — WebSocket upgrade routed to the DO. */
|
||||
/** GET /api/ws — WebSocket upgrade routed to the DO.
|
||||
* The DO routes by url.pathname, so we rewrite the URL to `/ws` while
|
||||
* preserving the original headers (including Upgrade) via the request init. */
|
||||
app.get('/api/ws', async (c) => {
|
||||
const upgradeHeader = c.req.header('Upgrade');
|
||||
if (upgradeHeader !== 'websocket') {
|
||||
return c.text('Expected WebSocket', 426);
|
||||
}
|
||||
return room(c.env).fetch(c.req.raw);
|
||||
return room(c.env).fetch('http://do/ws', c.req.raw);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user