43 lines
2.0 KiB
TypeScript
43 lines
2.0 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import { ApiError } from '../habits/service';
|
|
|
|
export class RenderBusyError extends ApiError {
|
|
constructor() { super(429, 'Image rendering is busy. Try again shortly.'); }
|
|
}
|
|
type Job = { run: () => Promise<Uint8Array>; resolve: (image: Uint8Array) => void; reject: (reason: unknown) => void };
|
|
/** One process-wide admission boundary for every server canvas renderer. */
|
|
export function createRenderQueue({ concurrency = 2, maxPending = 16, maxPerUser = 2 } = {}) {
|
|
const pending = new Map<string, Promise<Uint8Array>>();
|
|
const owners = new Map<string, number>();
|
|
const waiting: Job[] = [];
|
|
let active = 0;
|
|
function pump() {
|
|
while (active < concurrency && waiting.length) {
|
|
const job = waiting.shift()!; active++;
|
|
// Yield before drawing so a burst of callers cannot monopolize this turn.
|
|
setTimeout(() => {
|
|
void Promise.resolve().then(job.run).then(job.resolve, job.reject).finally(() => { active--; pump(); });
|
|
}, 0);
|
|
}
|
|
}
|
|
return (userId: string, key: string, run: Job['run']): Promise<Uint8Array> => {
|
|
const scopedKey = JSON.stringify([userId, key]);
|
|
const existing = pending.get(scopedKey);
|
|
if (existing) return existing;
|
|
if (pending.size >= maxPending || (owners.get(userId) ?? 0) >= maxPerUser) return Promise.reject(new RenderBusyError());
|
|
owners.set(userId, (owners.get(userId) ?? 0) + 1);
|
|
const work = new Promise<Uint8Array>((resolve, reject) => { waiting.push({ run, resolve, reject }); }).finally(() => {
|
|
pending.delete(scopedKey);
|
|
const count = owners.get(userId)! - 1;
|
|
if (count) owners.set(userId, count); else owners.delete(userId);
|
|
});
|
|
pending.set(scopedKey, work); pump();
|
|
return work;
|
|
};
|
|
}
|
|
const schedule = createRenderQueue();
|
|
export function renderImage(kind: string, userId: string, data: unknown, theme: string, run: Job['run']) {
|
|
const hash = createHash('sha256').update(JSON.stringify([kind, data, theme])).digest('hex');
|
|
return schedule(userId, hash, run);
|
|
}
|