Bound and deduplicate server image rendering per account
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { renderImage } from '../rendering/queue';
|
||||
import { cachedAvatar } from './avatar-cache';
|
||||
import { createCanvas, GlobalFonts } from "@napi-rs/canvas";
|
||||
import { existsSync } from "node:fs";
|
||||
@@ -48,7 +49,10 @@ export function liveCardLayout(count: number) {
|
||||
}
|
||||
|
||||
/** A deterministic 16:9 PNG. Every active habit receives a panel; none are paginated away. */
|
||||
export async function renderLiveCard(data: LiveImageData, theme: Theme) {
|
||||
export function renderLiveCard(data: LiveImageData, theme: Theme, userId = 'unscoped') {
|
||||
return renderImage('live', userId, data, theme, () => draw(data, theme));
|
||||
}
|
||||
async function draw(data: LiveImageData, theme: Theme) {
|
||||
const layout = liveCardLayout(data.habits.length);
|
||||
const canvas = createCanvas(layout.width, layout.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -127,5 +131,6 @@ export async function renderLiveCard(data: LiveImageData, theme: Theme) {
|
||||
rect(64, 1010, 1792, 1, colors.rule);
|
||||
text('Totals cover this entire period', 64, 1050, 28, 1200, colors.muted);
|
||||
ctx.textAlign = 'right'; text(`${data.habits.length} active habits`, 1856, 1050, 28, 536, colors.muted);
|
||||
return canvas.encode('png');
|
||||
try { return new Uint8Array(await canvas.encode('png')); }
|
||||
finally { canvas.width = 1; canvas.height = 1; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RenderBusyError } from '../rendering/queue';
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Hono } from "hono";
|
||||
@@ -81,7 +82,7 @@ export function createLiveDiscord(db: AppDatabase, auth: ReturnType<typeof creat
|
||||
guildId = channel.guild_id;
|
||||
} catch { update(id, { status: 'error', error: 'Could not reach the Discord channel. Try again.' }); return; }
|
||||
}
|
||||
const png = await renderLiveCard(data, current.theme);
|
||||
const png = await renderLiveCard(data, current.theme, id);
|
||||
if (png.byteLength > 4 * 1024 * 1024) { update(id, { status: 'error', error: 'The rendered image exceeds the 4 MB sharing limit.' }); return; }
|
||||
// A stop or account deletion during image generation cancels the outbound edit.
|
||||
if (stopped || !['active', 'sending'].includes(row(id)?.status ?? '')) return;
|
||||
@@ -139,7 +140,11 @@ export function createLiveDiscord(db: AppDatabase, auth: ReturnType<typeof creat
|
||||
clearTimeout(timers.get(id)); timers.delete(id);
|
||||
const existing = running.get(id);
|
||||
if (existing) return existing;
|
||||
const work = queue.then(() => deliver(id)).catch(() => {
|
||||
const work = queue.then(() => deliver(id)).catch(error => {
|
||||
if (error instanceof RenderBusyError && !stopped && ['active', 'sending'].includes(row(id)?.status ?? '')) {
|
||||
update(id, { dirty: true, retryAt: now() + 1000, error: 'Image rendering is busy; update queued.' });
|
||||
schedule(id, 1000); return;
|
||||
}
|
||||
if (!stopped && row(id)) { update(id, { status: 'error', error: 'Could not render or update the card. Try resuming updates.' }); }
|
||||
}).finally(() => running.delete(id));
|
||||
queue = work; running.set(id, work); return work;
|
||||
@@ -152,7 +157,7 @@ export function createLiveDiscord(db: AppDatabase, auth: ReturnType<typeof creat
|
||||
app.get('/', c => c.json(state(c.get('user').id)));
|
||||
app.get('/preview', async c => {
|
||||
const theme = c.req.query('theme') === 'dark' ? 'dark' : 'light';
|
||||
const image = await renderLiveCard(liveCardData(db, c.get('user').id, now()), theme);
|
||||
const image = await renderLiveCard(liveCardData(db, c.get('user').id, now()), theme, c.get('user').id);
|
||||
c.header('Content-Type', 'image/png'); c.header('Content-Disposition', 'inline; filename="minabot-live-progress.png"');
|
||||
return c.body(new Uint8Array(image));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user