fix: queue image resizing
This commit is contained in:
parent
3cfa2274a8
commit
3dc9692eef
82
lib/promise.ts
Normal file
82
lib/promise.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -7,9 +7,16 @@ import {
|
|||||||
import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts";
|
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 * as cache from "@lib/cache/image.ts";
|
||||||
import { SILVERBULLET_SERVER } from "@lib/env.ts";
|
import { SILVERBULLET_SERVER } from "@lib/env.ts";
|
||||||
|
import { PromiseQueue } from "@lib/promise.ts";
|
||||||
|
import { BadRequestError } from "@lib/errors.ts";
|
||||||
|
|
||||||
await initialize();
|
await initialize();
|
||||||
|
|
||||||
|
type ImageParams = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
async function getRemoteImage(image: string) {
|
async function getRemoteImage(image: string) {
|
||||||
console.log("[api/image] fetching", { image });
|
console.log("[api/image] fetching", { image });
|
||||||
const sourceRes = await fetch(image);
|
const sourceRes = await fetch(image);
|
||||||
@ -35,8 +42,8 @@ function getWidthHeight(
|
|||||||
: (current.width / final.width);
|
: (current.width / final.width);
|
||||||
|
|
||||||
return new MagickGeometry(
|
return new MagickGeometry(
|
||||||
current.width / ratio,
|
Math.floor(current.width / ratio),
|
||||||
current.height / 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 (
|
const GET = async (
|
||||||
_req: Request,
|
_req: Request,
|
||||||
_ctx: HandlerContext,
|
_ctx: HandlerContext,
|
||||||
@ -113,35 +149,13 @@ const GET = async (
|
|||||||
console.log("[api/image] no image in cache");
|
console.log("[api/image] no image in cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteImage = await getRemoteImage(imageUrl);
|
const [resizedImage, mediaType] = await queue.enqueue(() =>
|
||||||
if (typeof remoteImage === "string") {
|
processImage(imageUrl, params)
|
||||||
console.log("[api/image] ERROR " + remoteImage);
|
);
|
||||||
return new Response(remoteImage, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifiedImage = await modifyImage(remoteImage.buffer, {
|
return new Response(resizedImage, {
|
||||||
...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, {
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": remoteImage.mediaType,
|
"Content-Type": mediaType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user