From fac0a388583828b8dc8e91ffef8be260a3de0d9f Mon Sep 17 00:00:00 2001 From: Sugui Date: Sun, 21 Apr 2024 11:49:55 +0200 Subject: [PATCH] Refactored all classes as modules, and added thread-safetyness to imageService --- bun.lockb | Bin 53938 -> 54645 bytes package.json | 1 + src/app.ts | 4 +- src/controllers/ImageController.ts | 17 ---- src/controllers/imageController.ts | 13 ++++ src/services/BotApiService.ts | 22 ------ src/services/GelbooruApiService.ts | 32 -------- src/services/ImageService.ts | 39 ---------- src/services/botApiService.ts | 18 +++++ src/services/gelbooruApiService.ts | 28 +++++++ src/services/imageService.ts | 51 ++++++++++++ test/ImageController/ImageController.test.ts | 27 ++----- test/ImageService/ImageService.test.ts | 77 +++++++------------ yarn.lock | 14 +++- 14 files changed, 160 insertions(+), 183 deletions(-) delete mode 100644 src/controllers/ImageController.ts create mode 100644 src/controllers/imageController.ts delete mode 100644 src/services/BotApiService.ts delete mode 100644 src/services/GelbooruApiService.ts delete mode 100644 src/services/ImageService.ts create mode 100644 src/services/botApiService.ts create mode 100644 src/services/gelbooruApiService.ts create mode 100644 src/services/imageService.ts diff --git a/bun.lockb b/bun.lockb index c74810a397fb96f52d91e4a1e6618c9d7423d868..347f41e4f365a6f2ab3c1d172580761089bcd6e2 100755 GIT binary patch delta 7788 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 delta 7278 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 { res.json({ endpoints }); }); -app.get("/image", ImageController.get); +app.get("/image", imageController.get); export default app; diff --git a/src/controllers/ImageController.ts b/src/controllers/ImageController.ts deleted file mode 100644 index 9a6bcf5..0000000 --- a/src/controllers/ImageController.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Request, Response } from "express"; -import logger from "src/logger"; -import ImageService from "src/services/ImageService"; - -class ImageController { - async get(_: Request, res: Response) { - try { - const image = await ImageService.get(); - res.json(image); - } catch (error: any) { - logger.error(error); - res.status(500).json({ error: `Internal server error: ${error}` }); - } - } -} - -export default new ImageController(); diff --git a/src/controllers/imageController.ts b/src/controllers/imageController.ts new file mode 100644 index 0000000..e0e8bd6 --- /dev/null +++ b/src/controllers/imageController.ts @@ -0,0 +1,13 @@ +import { Request, Response } from "express"; +import logger from "src/logger"; +import * as imageService from "src/services/imageService"; + +export async function get(_: Request, res: Response) { + try { + const image = await imageService.get(); + res.json(image); + } catch (error: any) { + logger.error(error); + res.status(500).json({ error: `Internal server error: ${error}` }); + } +} diff --git a/src/services/BotApiService.ts b/src/services/BotApiService.ts deleted file mode 100644 index 85b0b8b..0000000 --- a/src/services/BotApiService.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { env } from "bun"; -import logger from "src/logger"; -import { BotApiResponse } from "src/types/BotApiResponse"; - -class BotApiService { - readonly BOT_API_URI = env.BOT_API_URI; - - async getAll(): Promise { - const get_url = `${this.BOT_API_URI}/images`; - const response: BotApiResponse = (await fetch(get_url).then(async (res) => { - if (!res.ok) { - logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`); - throw new Error("Error fetching images"); - } else { - return res.json(); - } - })) as BotApiResponse; - return response; - } -} - -export default new BotApiService(); diff --git a/src/services/GelbooruApiService.ts b/src/services/GelbooruApiService.ts deleted file mode 100644 index 509f472..0000000 --- a/src/services/GelbooruApiService.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { env } from "bun"; -import logger from "src/logger"; -import GelbooruApiResponse from "src/types/GelbooruApiResponse"; -import GelbooruServiceResponse from "src/types/GelbooruServiceResponse"; - -class GelbooruApiService { - async get(): Promise { - const LIMIT = env.GELBOORU_IMAGES_PER_REQUEST || 100; - const TAGS = encodeURIComponent(env.GELBOORU_TAGS || ""); - const url: string = `https://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=${LIMIT}&json=1&tags=${TAGS}`; - - const response: GelbooruApiResponse = (await fetch(url).then( - async (res) => { - if (!res.ok) { - logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`); - throw new Error("Error fetching images"); - } else { - return res.json(); - } - } - )) as GelbooruApiResponse; - - return { - posts: response.post.map((post) => ({ - url: post.file_url, - tags: post.tags.split(" "), - })), - }; - } -} - -export default new GelbooruApiService(); diff --git a/src/services/ImageService.ts b/src/services/ImageService.ts deleted file mode 100644 index 53005fb..0000000 --- a/src/services/ImageService.ts +++ /dev/null @@ -1,39 +0,0 @@ -import GelbooruServiceResponse from "src/types/GelbooruServiceResponse"; -import Image from "src/types/Image"; -import BotApiService from "src/services/BotApiService"; -import GelbooruApiService from "src/services/GelbooruApiService"; -import logger from "src/logger"; - -class ImageService { - postsQueue: Image[] = []; - - async get(): Promise { - while (this.postsQueue.length === 0) { - const validPosts = await this.getNewValidImages(); - this.postsQueue = validPosts; - logger.info(`Got ${validPosts.length} images from remote`); - } - return this.postsQueue.pop() as Image; - } - - private async getNewValidImages(): Promise { - const gelbooruResponse: GelbooruServiceResponse = - await GelbooruApiService.get(); - const posts = gelbooruResponse.posts; - - const botResponse = await BotApiService.getAll(); - const imagesUrls = botResponse.images.map((image) => image.url); - - const validPosts = Promise.all( - posts - .filter((post) => !imagesUrls.some((url) => url === post.url)) - .map((post): Image => { - return { url: post.url, tags: post.tags }; - }) - ); - - return validPosts; - } -} - -export default new ImageService(); diff --git a/src/services/botApiService.ts b/src/services/botApiService.ts new file mode 100644 index 0000000..b7da92e --- /dev/null +++ b/src/services/botApiService.ts @@ -0,0 +1,18 @@ +import { env } from "bun"; +import logger from "src/logger"; +import { BotApiResponse } from "src/types/BotApiResponse"; + +const BOT_API_URI = env.BOT_API_URI; + +export async function getAll(): Promise { + const get_url = `${BOT_API_URI}/images`; + const response: BotApiResponse = (await fetch(get_url).then(async (res) => { + if (!res.ok) { + logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`); + throw new Error("Error fetching images"); + } else { + return res.json(); + } + })) as BotApiResponse; + return response; +} diff --git a/src/services/gelbooruApiService.ts b/src/services/gelbooruApiService.ts new file mode 100644 index 0000000..f584892 --- /dev/null +++ b/src/services/gelbooruApiService.ts @@ -0,0 +1,28 @@ +import { env } from "bun"; +import logger from "src/logger"; +import GelbooruApiResponse from "src/types/GelbooruApiResponse"; +import GelbooruServiceResponse from "src/types/GelbooruServiceResponse"; + +export async function get(): Promise { + const LIMIT = env.GELBOORU_IMAGES_PER_REQUEST || 100; + const TAGS = encodeURIComponent(env.GELBOORU_TAGS || ""); + const url: string = `https://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=${LIMIT}&json=1&tags=${TAGS}`; + + const response: GelbooruApiResponse = (await fetch(url).then( + async (res) => { + if (!res.ok) { + logger.error(`${res.status}: ${res.statusText}, ${await res.text()}`); + throw new Error("Error fetching images"); + } else { + return res.json(); + } + } + )) as GelbooruApiResponse; + + return { + posts: response.post.map((post) => ({ + url: post.file_url, + tags: post.tags.split(" "), + })), + }; +} \ No newline at end of file diff --git a/src/services/imageService.ts b/src/services/imageService.ts new file mode 100644 index 0000000..ea20d53 --- /dev/null +++ b/src/services/imageService.ts @@ -0,0 +1,51 @@ +import GelbooruServiceResponse from "src/types/GelbooruServiceResponse"; +import Image from "src/types/Image"; +import logger from "src/logger"; +import { Mutex } from "async-mutex"; +import * as gelbooruApiService from 'src/services/gelbooruApiService'; +import * as botApiService from 'src/services/botApiService'; + +const mutex: Mutex = new Mutex(); +const postsQueue: Image[] = []; + +// We wrap the function into a Mutex because it's not thread-safe +export async function get(): Promise { + return await mutex.runExclusive(() => unsafeGet()) +} + +async function unsafeGet(): Promise { + while (postsQueue.length === 0) { + const validPosts = await getNewValidImages(); + validPosts.map(post => postsQueue.push(post)); + logger.info(`Got ${validPosts.length} images from remote`); + } + return popImage(); +} + +function popImage(): Image { + const image = postsQueue.pop(); + if (image) { + return image + } else { + throw Error("Can't pop from an empty list"); + } +} + +async function getNewValidImages(): Promise { + const gelbooruResponse: GelbooruServiceResponse = + await gelbooruApiService.get(); + const posts = gelbooruResponse.posts; + + const botResponse = await botApiService.getAll(); + const imagesUrls = botResponse.images.map((image) => image.url); + + const validPosts = Promise.all( + posts + .filter((post) => !imagesUrls.some((url) => url === post.url)) + .map((post): Image => { + return { url: post.url, tags: post.tags }; + }) + ); + + return validPosts; +} diff --git a/test/ImageController/ImageController.test.ts b/test/ImageController/ImageController.test.ts index ddf1ad5..4c8746b 100644 --- a/test/ImageController/ImageController.test.ts +++ b/test/ImageController/ImageController.test.ts @@ -1,16 +1,11 @@ -import { afterEach, describe, expect, it, mock } from "bun:test"; +import { afterAll, afterEach, describe, expect, it, jest, mock, spyOn } from "bun:test"; import app from "src/app"; -import ImageService from "src/services/ImageService"; import request from "supertest"; +import * as imageService from "src/services/imageService"; -const imageServiceOriginal = ImageService; - -afterEach(() => { - mock.restore(); - mock.module("src/services/ImageService", () => ({ - default: imageServiceOriginal, - })); -}); +afterAll(() => { + jest.restoreAllMocks(); +}) describe("endpoint returns the correct status codes", () => { it("should return 200 if successful", async () => { @@ -21,16 +16,10 @@ describe("endpoint returns the correct status codes", () => { }); it("should return 500 if any error happens", async () => { - mock.module("src/services/ImageService", () => { - return { - default: { - get: () => { - throw new Error("Controlled error"); - }, - }, - }; - }); + spyOn(imageService, "get").mockImplementation(() => { throw new Error("Controlled error") }); + const res = await request(app).get("/image"); + expect(res.status).toBe(500); }); }); diff --git a/test/ImageService/ImageService.test.ts b/test/ImageService/ImageService.test.ts index 1f48d98..67e36c0 100644 --- a/test/ImageService/ImageService.test.ts +++ b/test/ImageService/ImageService.test.ts @@ -1,27 +1,18 @@ -import { afterEach, describe, expect, it, mock } from "bun:test"; -import BotApiService from "src/services/BotApiService"; -import GelbooruApiService from "src/services/GelbooruApiService"; -import ImageService from "src/services/ImageService"; -import { BotApiResponse } from "src/types/BotApiResponse"; -import GelbooruApiResponse from "src/types/GelbooruServiceResponse"; +import { afterAll, describe, expect, it, jest, mock, spyOn } from "bun:test"; import Image from "src/types/Image"; +import * as gelbooruApiService from "src/services/gelbooruApiService"; +import * as botApiService from "src/services/botApiService"; +import * as imageService from "src/services/imageService"; -const imageServiceOriginal = ImageService; -const gelbooruApiServiceOriginal = GelbooruApiService; -const botApiServiceOriginal = BotApiService; +afterAll(() => { + jest.restoreAllMocks(); +}) -afterEach(() => { - mock.restore(); - mock.module("src/services/ImageService", () => ({ - default: imageServiceOriginal, - })); - mock.module("src/services/GelbooruApiService", () => ({ - default: gelbooruApiServiceOriginal, - })); - mock.module("src/services/BotApiService", () => ({ - default: botApiServiceOriginal, - })); -}); +describe("the service is thread-safe", () => { + it("should run normally when 2 processes call the get() method with 1 remaining image in the queue", async () => { + // TODO + }) +}) describe("endpoint gets a non repeated image", () => { it("should return an image that is not in the response of the /images endpoint of the bot API", async () => { @@ -30,39 +21,23 @@ describe("endpoint gets a non repeated image", () => { const UNIQUE_URL = "https://fastly.picsum.photos/id/2/10/20.jpg?hmac=zy6lz21CuRIstr9ETx9h5AuoH50s_L2uIEct3dROpY8"; - mock.module("src/services/GelbooruApiService", () => { - let alreadyCalled = false; - return { - default: { - get: (): GelbooruApiResponse => { - if (alreadyCalled) { - return { posts: [{ url: UNIQUE_URL, tags: [] }] }; - } else { - alreadyCalled = true; - return { posts: [{ url: REPEATED_URL, tags: [] }] }; - } - }, + const gelbooruApiServiceGet = spyOn(gelbooruApiService, "get"); + gelbooruApiServiceGet.mockImplementationOnce(async () => ({ posts: [{ url: UNIQUE_URL, tags: [] }] })); + gelbooruApiServiceGet.mockImplementation(async () => ({ posts: [{ url: REPEATED_URL, tags: [] }] })); + spyOn(botApiService, "getAll").mockImplementation(async () => ({ + images: [ + { + _id: "0", + url: REPEATED_URL, + status: "consumed", + tags: ["pokemon", "computer"], + __v: 0, }, - }; - }); - - mock.module("src/services/BotApiService", () => ({ - default: { - getAll: (): BotApiResponse => ({ - images: [ - { - _id: "0", - url: REPEATED_URL, - status: "consumed", - tags: ["pokemon", "computer"], - __v: 0, - }, - ], - }), - }, + ], })); - const image: Image = await ImageService.get(); + const image: Image = await imageService.get(); + expect(image.url).not.toBe(REPEATED_URL); }); }); diff --git a/yarn.lock b/yarn.lock index 5db6c5e..ff9aa21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6 +1,6 @@ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 -# bun ./bun.lockb --hash: 2D4E664D3091262E-6e6ac083a9fdd796-C67F3D33680268C0-c674fd6e89aeea1f +# bun ./bun.lockb --hash: 9A5A612BBFD3E7ED-76a31de2b7c4bbe8-8DB93E498B9CBB30-9cf1116024820c4f "@colors/colors@1.6.0", "@colors/colors@^1.6.0": @@ -177,6 +177,13 @@ async@^3.2.3: resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== +async-mutex@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz" + integrity sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA== + dependencies: + tslib "^2.4.0" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -908,6 +915,11 @@ triple-beam@^1.3.0: resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== +tslib@^2.4.0: + version "2.6.2" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz"