diff --git a/lib/promise.ts b/lib/promise.ts new file mode 100644 index 0000000..b308c3d --- /dev/null +++ b/lib/promise.ts @@ -0,0 +1,82 @@ +/** + * Interface zur Beschreibung eines eingereihten Promises in der `PromiseQueue`. + */ +interface QueuedPromise { + promise: () => Promise; + resolve: (value: T) => void; + reject: (reason?: any) => void; +} + +/** + * Eine einfache Promise Queue, die es ermöglicht mehrere Aufgaben in kontrollierter + * Reihenfolge abzuarbeiten. + * + * Lizenz: CC BY-NC-SA 4.0 + * (c) Peter Müller (https://crycode.de/promise-queue-in-typescript) + */ +export class PromiseQueue { + /** + * Eingereihte Promises. + */ + private queue: QueuedPromise[] = []; + + /** + * Indikator, dass aktuell ein Promise abgearbeitet wird. + */ + private working = false; + + /** + * Ein Promise einreihen. + * Dies fügt das Promise der Warteschlange hinzu. Wenn die Warteschlange leer + * ist, dann wird das Promise sofort gestartet. + * @param promise Funktion, die das Promise zurückgibt. + * @returns Ein Promise, welches eingelöst (oder zurückgewiesen) wird sobald das eingereihte Promise abgearbeitet ist. + */ + public enqueue(promise: () => Promise): Promise { + return new Promise((resolve, reject) => { + this.queue.push({ + promise, + resolve, + reject, + }); + this.dequeue(); + }); + } + + /** + * Das erste Promise aus der Warteschlange holen und starten, sofern nicht + * bereits ein Promise aktiv ist. + * @returns `true` wenn ein Promise aus der Warteschlange gestartet wurde oder `false` wenn bereits ein Promise aktiv oder die Warteschlange leer ist. + */ + private dequeue(): boolean { + if (this.working) { + return false; + } + + const item = this.queue.shift(); + if (!item) { + return false; + } + + try { + this.working = true; + item.promise() + .then((value) => { + item.resolve(value); + }) + .catch((err) => { + item.reject(err); + }) + .finally(() => { + this.working = false; + this.dequeue(); + }); + } catch (err) { + item.reject(err); + this.working = false; + this.dequeue(); + } + + return true; + } +} diff --git a/routes/api/images/index.ts b/routes/api/images/index.ts index 2e2bc91..64ac416 100644 --- a/routes/api/images/index.ts +++ b/routes/api/images/index.ts @@ -7,9 +7,16 @@ import { import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts"; import * as cache from "@lib/cache/image.ts"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; +import { PromiseQueue } from "@lib/promise.ts"; +import { BadRequestError } from "@lib/errors.ts"; await initialize(); +type ImageParams = { + width: number; + height: number; +}; + async function getRemoteImage(image: string) { console.log("[api/image] fetching", { image }); const sourceRes = await fetch(image); @@ -35,8 +42,8 @@ function getWidthHeight( : (current.width / final.width); return new MagickGeometry( - current.width / ratio, - current.height / ratio, + Math.floor(current.width / ratio), + Math.floor(current.height / ratio), ); } @@ -82,6 +89,35 @@ function parseParams(reqUrl: URL) { }; } +const queue = new PromiseQueue(); + +async function processImage(imageUrl: string, params: ImageParams) { + const remoteImage = await getRemoteImage(imageUrl); + if (typeof remoteImage === "string") { + console.log("[api/image] ERROR " + remoteImage); + throw new BadRequestError(); + } + + const modifiedImage = await modifyImage(remoteImage.buffer, { + ...params, + mode: "resize", + }); + + console.log("[api/image] resized image", { + imageUrl, + length: modifiedImage.length, + }); + + await cache.setImage(modifiedImage.slice(), { + url: imageUrl, + width: params.width, + height: params.height, + mediaType: remoteImage.mediaType, + }); + + return [modifiedImage, remoteImage.mediaType] as const; +} + const GET = async ( _req: Request, _ctx: HandlerContext, @@ -113,35 +149,13 @@ const GET = async ( console.log("[api/image] no image in cache"); } - const remoteImage = await getRemoteImage(imageUrl); - if (typeof remoteImage === "string") { - console.log("[api/image] ERROR " + remoteImage); - return new Response(remoteImage, { status: 400 }); - } + const [resizedImage, mediaType] = await queue.enqueue(() => + processImage(imageUrl, params) + ); - const modifiedImage = await modifyImage(remoteImage.buffer, { - ...params, - mode: "resize", - }); - - console.log("[api/image] resized image", { imageUrl }); - - await cache.setImage(modifiedImage, { - url: imageUrl, - width: params.width, - height: params.height, - mediaType: remoteImage.mediaType, - }); - - const cachedImage = await cache.getImage({ - url: imageUrl, - width: params.width, - height: params.height, - }); - - return new Response(cachedImage.data || modifiedImage, { + return new Response(resizedImage, { headers: { - "Content-Type": remoteImage.mediaType, + "Content-Type": mediaType, }, }); };