Helper for cache related utilities (#6696)
Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
@@ -1,57 +1,22 @@
|
|||||||
import type { Unfurl } from "@shared/types";
|
import type { Unfurl } from "@shared/types";
|
||||||
import { Day } from "@shared/utils/time";
|
|
||||||
import { InternalError } from "@server/errors";
|
import { InternalError } from "@server/errors";
|
||||||
import Logger from "@server/logging/Logger";
|
|
||||||
import Redis from "@server/storage/redis";
|
|
||||||
import fetch from "@server/utils/fetch";
|
import fetch from "@server/utils/fetch";
|
||||||
import env from "./env";
|
import env from "./env";
|
||||||
|
|
||||||
class Iframely {
|
class Iframely {
|
||||||
private static apiUrl = `${env.IFRAMELY_URL}/api`;
|
private static apiUrl = `${env.IFRAMELY_URL}/api`;
|
||||||
private static apiKey = env.IFRAMELY_API_KEY;
|
private static apiKey = env.IFRAMELY_API_KEY;
|
||||||
private static cacheKeyPrefix = "unfurl";
|
|
||||||
private static defaultCacheExpiry = Day;
|
|
||||||
|
|
||||||
private static cacheKey(url: string) {
|
|
||||||
return `${this.cacheKeyPrefix}-${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async cache(url: string, response: any) {
|
|
||||||
// do not cache error responses
|
|
||||||
if (response.error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await Redis.defaultClient.set(
|
|
||||||
this.cacheKey(url),
|
|
||||||
JSON.stringify(response),
|
|
||||||
"EX",
|
|
||||||
response.cache_age || this.defaultCacheExpiry
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
// just log it, can skip caching and directly return response
|
|
||||||
Logger.error("Could not cache Iframely response", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async fetch(url: string, type = "oembed") {
|
public static async fetch(url: string, type = "oembed") {
|
||||||
const res = await fetch(
|
|
||||||
`${this.apiUrl}/${type}?url=${encodeURIComponent(url)}&api_key=${
|
|
||||||
this.apiKey
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async cached(url: string) {
|
|
||||||
try {
|
try {
|
||||||
const val = await Redis.defaultClient.get(this.cacheKey(url));
|
const res = await fetch(
|
||||||
if (val) {
|
`${this.apiUrl}/${type}?url=${encodeURIComponent(url)}&api_key=${
|
||||||
return JSON.parse(val);
|
this.apiKey
|
||||||
}
|
}`
|
||||||
|
);
|
||||||
|
return res.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// just log it, response can still be obtained using the fetch call
|
throw InternalError(err);
|
||||||
Logger.error("Could not fetch cached Iframely response", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,18 +26,8 @@ class Iframely {
|
|||||||
* @param url
|
* @param url
|
||||||
* @returns Preview data for the url
|
* @returns Preview data for the url
|
||||||
*/
|
*/
|
||||||
public static async get(url: string): Promise<Unfurl | false> {
|
public static async unfurl(url: string): Promise<Unfurl | false> {
|
||||||
try {
|
return Iframely.fetch(url);
|
||||||
const cached = await Iframely.cached(url);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
const res = await Iframely.fetch(url);
|
|
||||||
await Iframely.cache(url, res);
|
|
||||||
return res;
|
|
||||||
} catch (err) {
|
|
||||||
throw InternalError(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Day } from "@shared/utils/time";
|
||||||
import {
|
import {
|
||||||
PluginManager,
|
PluginManager,
|
||||||
PluginPriority,
|
PluginPriority,
|
||||||
@@ -12,7 +13,7 @@ if (enabled) {
|
|||||||
PluginManager.add([
|
PluginManager.add([
|
||||||
{
|
{
|
||||||
type: Hook.UnfurlProvider,
|
type: Hook.UnfurlProvider,
|
||||||
value: Iframely.get,
|
value: { unfurl: Iframely.unfurl, cacheExpiry: Day },
|
||||||
|
|
||||||
// Make sure this is last in the stack to be evaluated after all other unfurl providers
|
// Make sure this is last in the stack to be evaluated after all other unfurl providers
|
||||||
priority: PluginPriority.VeryLow,
|
priority: PluginPriority.VeryLow,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { authorize } from "@server/policies";
|
|||||||
import { presentDocument, presentMention } from "@server/presenters/unfurls";
|
import { presentDocument, presentMention } from "@server/presenters/unfurls";
|
||||||
import presentUnfurl from "@server/presenters/unfurls/unfurl";
|
import presentUnfurl from "@server/presenters/unfurls/unfurl";
|
||||||
import { APIContext } from "@server/types";
|
import { APIContext } from "@server/types";
|
||||||
|
import { CacheHelper } from "@server/utils/CacheHelper";
|
||||||
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
||||||
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
|
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
|
||||||
import * as T from "./schema";
|
import * as T from "./schema";
|
||||||
@@ -75,12 +76,26 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// External resources
|
// External resources
|
||||||
|
const cachedData = await CacheHelper.getData(
|
||||||
|
CacheHelper.getUnfurlKey(url, actor.teamId)
|
||||||
|
);
|
||||||
|
if (cachedData) {
|
||||||
|
return (ctx.body = presentUnfurl(cachedData));
|
||||||
|
}
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
const data = await plugin.value(url);
|
const data = await plugin.value.unfurl(url);
|
||||||
if (data) {
|
if (data) {
|
||||||
return "error" in data
|
if ("error" in data) {
|
||||||
? (ctx.response.status = 204)
|
return (ctx.response.status = 204);
|
||||||
: (ctx.body = presentUnfurl(data));
|
} else {
|
||||||
|
await CacheHelper.setData(
|
||||||
|
CacheHelper.getUnfurlKey(url, actor.teamId),
|
||||||
|
data,
|
||||||
|
plugin.value.cacheExpiry
|
||||||
|
);
|
||||||
|
return (ctx.body = presentUnfurl(data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
server/utils/CacheHelper.ts
Normal file
66
server/utils/CacheHelper.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Day } from "@shared/utils/time";
|
||||||
|
import Logger from "@server/logging/Logger";
|
||||||
|
import Redis from "@server/storage/redis";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Helper class for server-side cache management
|
||||||
|
*/
|
||||||
|
export class CacheHelper {
|
||||||
|
private static defaultDataExpiry = Day;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a key, gets the data from cache store
|
||||||
|
*
|
||||||
|
* @param key Key against which data will be accessed
|
||||||
|
*/
|
||||||
|
public static async getData(key: string) {
|
||||||
|
try {
|
||||||
|
const data = await Redis.defaultClient.get(key);
|
||||||
|
if (data) {
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// just log it, response can still be obtained using the fetch call
|
||||||
|
Logger.error(`Could not fetch cached response against ${key}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a key, data and cache config, saves the data in cache store
|
||||||
|
*
|
||||||
|
* @param key Cache key
|
||||||
|
* @param data Data to be saved against the key
|
||||||
|
* @param expiry Cache data expiry
|
||||||
|
*/
|
||||||
|
public static async setData(
|
||||||
|
key: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
expiry?: number
|
||||||
|
) {
|
||||||
|
if ("error" in data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Redis.defaultClient.set(
|
||||||
|
key,
|
||||||
|
JSON.stringify(data),
|
||||||
|
"EX",
|
||||||
|
expiry || this.defaultDataExpiry
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// just log it, can skip caching and directly return response
|
||||||
|
Logger.error(`Could not cache response against ${key}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets key against which unfurl response for the given url is stored
|
||||||
|
*
|
||||||
|
* @param url The url to generate a key for
|
||||||
|
* @param teamId The team ID to generate a key for
|
||||||
|
*/
|
||||||
|
public static getUnfurlKey(url: string, teamId: string) {
|
||||||
|
return `unfurl:${teamId}:${url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ type PluginValueMap = {
|
|||||||
[Hook.EmailTemplate]: typeof BaseEmail;
|
[Hook.EmailTemplate]: typeof BaseEmail;
|
||||||
[Hook.Processor]: typeof BaseProcessor;
|
[Hook.Processor]: typeof BaseProcessor;
|
||||||
[Hook.Task]: typeof BaseTask<any>;
|
[Hook.Task]: typeof BaseTask<any>;
|
||||||
[Hook.UnfurlProvider]: UnfurlSignature;
|
[Hook.UnfurlProvider]: { unfurl: UnfurlSignature; cacheExpiry: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Plugin<T extends Hook> = {
|
export type Plugin<T extends Hook> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user