fix: queue image resizing

This commit is contained in:
max_richter 2023-08-02 02:24:08 +02:00
parent 3cfa2274a8
commit 3dc9692eef
2 changed files with 125 additions and 29 deletions

82
lib/promise.ts Normal file
View File

@ -0,0 +1,82 @@
/**
* Interface zur Beschreibung eines eingereihten Promises in der `PromiseQueue`.
*/
interface QueuedPromise<T = any> {
promise: () => Promise<T>;
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 <peter@crycode.de> (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<T = void>(promise: () => Promise<T>): Promise<T> {
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;
}
}

View File

@ -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,
},
});
};