mirror of
https://github.com/spacebarchat/server.git
synced 2026-07-13 07:19:03 +00:00
Add SingletonCache, implement caching for tenor
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2026 Spacebar and Spacebar Contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { TimeSpan } from "./Timespan";
|
||||
import { sleep } from "./index";
|
||||
|
||||
export class SingletonCache<T> {
|
||||
private expiry: TimeSpan;
|
||||
private lastUpdated: Date = new Date(0);
|
||||
private isLocked: boolean = false;
|
||||
private cachedValue: T;
|
||||
|
||||
constructor(expiry: TimeSpan) {
|
||||
this.expiry = expiry;
|
||||
}
|
||||
|
||||
async getOrUpdate(factory: () => Promise<T>): Promise<T> {
|
||||
if (new TimeSpan(this.lastUpdated.getTime(), Date.now()).totalMillis < this.expiry.totalMillis) return this.cachedValue;
|
||||
|
||||
if (this.isLocked) {
|
||||
// avoid running the factory twice (async)
|
||||
while (this.isLocked) await sleep(50);
|
||||
return this.cachedValue;
|
||||
}
|
||||
|
||||
this.isLocked = true;
|
||||
try {
|
||||
const result = await factory();
|
||||
this.cachedValue = result;
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.log(`[SingletonCache] Factory method failed, returning stale value:`, e);
|
||||
return this.cachedValue;
|
||||
} finally {
|
||||
this.isLocked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2026 Spacebar and Spacebar Contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export async function sleep(ms: number) {
|
||||
return new Promise((resolve) => void setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -22,12 +22,9 @@ export * from "./DateBuilder";
|
||||
export * from "./ElapsedTime";
|
||||
export * from "./Math";
|
||||
export * from "./Random";
|
||||
export * from "./SingletonCache";
|
||||
export * from "./Sleep";
|
||||
export * from "./Stopwatch";
|
||||
export * from "./String";
|
||||
export * from "./Timespan";
|
||||
export * from "./Url";
|
||||
|
||||
// TODO: move to a separate file
|
||||
export async function sleep(ms: number) {
|
||||
return new Promise((resolve) => void setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -19,12 +19,18 @@
|
||||
import type { IGifProvider } from "../IGifProvider";
|
||||
import { GifsResponse, GifTrendingCategory, GifMediaTypes } from "@spacebar/schemas";
|
||||
import { Config } from "@spacebar/util";
|
||||
import { SingletonCache, TimeSpan } from "@spacebar/extensions";
|
||||
|
||||
const TRENDING_CATEGORIES_CACHE_DURATION = TimeSpan.fromSeconds(24 * 60 * 60); // 1 day
|
||||
const TRENDING_GIFS_CACHE_DURATION = TimeSpan.fromSeconds(60 * 60); // 1 hour
|
||||
|
||||
// Tenor V1 API... Not yet shut down as of writing, but is going soon...
|
||||
export default class TenorGifProvider implements IGifProvider {
|
||||
id = "tenor";
|
||||
available = true;
|
||||
#apiKey: string;
|
||||
#trendingCategoryCache = new SingletonCache<GifTrendingCategory[]>(TRENDING_CATEGORIES_CACHE_DURATION);
|
||||
#trendingGifsCache = new SingletonCache<GifsResponse>(TRENDING_GIFS_CACHE_DURATION);
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (!(Config.get().gif.enabled && Config.get().gif.apiKey)) {
|
||||
@@ -47,26 +53,31 @@ export default class TenorGifProvider implements IGifProvider {
|
||||
}
|
||||
|
||||
async getTrendingCategories(query: { media_format: string; locale: string }): Promise<GifTrendingCategory[]> {
|
||||
const response = await fetch(`https://g.tenor.com/v1/categories?locale=${query.locale}&key=${this.#apiKey}`, {
|
||||
method: "get",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
return await this.#trendingCategoryCache.getOrUpdate(async () => {
|
||||
const response = await fetch(`https://g.tenor.com/v1/categories?locale=${query.locale}&key=${this.#apiKey}`, {
|
||||
method: "get",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const responseData = (await response.json()) as TenorCategoriesResults;
|
||||
return responseData.tags.map((x) => ({
|
||||
name: x.searchterm,
|
||||
src: x.image,
|
||||
})) satisfies GifTrendingCategory[];
|
||||
const responseData = (await response.json()) as TenorCategoriesResults;
|
||||
return responseData.tags.map((x) => ({
|
||||
name: x.searchterm,
|
||||
src: x.image,
|
||||
})) satisfies GifTrendingCategory[];
|
||||
});
|
||||
}
|
||||
|
||||
async getTrendingGifs(query: { media_format: string; locale: string }): Promise<GifsResponse> {
|
||||
const response = await fetch(`https://g.tenor.com/v1/trending?media_format=${query.media_format}&locale=${query.locale}&key=${this.#apiKey}`, {
|
||||
method: "get",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
return await this.#trendingGifsCache.getOrUpdate(async () => {
|
||||
const response = await fetch(`https://g.tenor.com/v1/trending?media_format=${query.media_format}&locale=${query.locale}&key=${this.#apiKey}`, {
|
||||
method: "get",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const responseData = (await response.json()) as TenorTrendingResults;
|
||||
return responseData.results.map(this.convertGifResult);
|
||||
console.log(response);
|
||||
const responseData = (await response.json()) as TenorTrendingResults;
|
||||
return responseData.results.map(this.convertGifResult);
|
||||
});
|
||||
}
|
||||
|
||||
private convertGifResult(result: TenorGif) {
|
||||
|
||||
Reference in New Issue
Block a user