diff --git a/config/storages.ts b/config/storages.ts new file mode 100644 index 0000000000..f00152c890 --- /dev/null +++ b/config/storages.ts @@ -0,0 +1,92 @@ +import type { StorageMounts } from 'nitropack' + +// https://nuxt.com/docs/api/configuration/nuxt-config +declare global { + namespace NodeJS { + interface ProcessEnv { + /** Preset used to build Nitro (provided manually). */ + NITRO_PRESET?: string + + /** Cloudflare KV via binding: name of the binding. */ + CF_KV_BINDING_CACHE?: string + + /** Vercel KV: token. */ + KV_REST_API_TOKEN?: string + /** Vercel KV: API URL. */ + KV_REST_API_URL?: string + /** + * Vercel KV: base name for cache KV. + * @default 'cache' + */ + VERCEL_KV_CACHE_BASE?: string + + /** Cache storage option. */ + CACHE_STORAGE_OPTION?: string + } + } +} + +/** + * Checks that all environment variables are defined. + * @param vars Variables to check. + * @returns All missing variables. + */ +function getMissingVars(vars: string[]) { + return vars.filter((varName) => !process.env[varName]) +} + +/** + * Returns Nitro storage mounts or nothing. + */ +function getCacheStorageMount(): StorageMounts[string] | undefined { + switch (process.env.CACHE_STORAGE_OPTION) { + case 'cloudflare-kv': { + if (process.env.CF_KV_BINDING_CACHE) { + return { + driver: '~/server/storage/cached-cloudflare-kv-binding', + binding: process.env.CF_KV_BINDING_CACHE, + } + } + + console.warn( + 'You wanted to use `cloudflare-kv` cache store option, however you have not provided `CF_KV_BINDING_CACHE` environment variable. The cache will use in-memory storage that is not persistent in workers.' + ) + + break + } + case 'vercel-kv': { + const missingVars = getMissingVars(['KV_REST_API_TOKEN', 'KV_REST_API_URL']) + + if (!missingVars.length) { + return { + driver: '~/server/storage/cached-vercel-kv', + base: process.env.VERCEL_KV_CACHE_BASE || 'cache', + url: process.env.KV_REST_API_URL, + token: process.env.KV_REST_API_TOKEN, + env: false, + } + } + + console.log( + `You wanted to use \`vercel-kv\` cache store option, however you have not provided ${missingVars + .map((varName) => `\`${varName}\``) + .join( + ', ' + )} environment variable. The cache will use in-memory storage taht is not persistent in serverless functions.` + ) + + break + } + } + + return undefined +} + +export function getStorageMounts(): StorageMounts | undefined { + let mounts: StorageMounts | undefined + + const cacheMount = getCacheStorageMount() + if (cacheMount != null) (mounts ??= {}).cache = cacheMount + + return mounts +} diff --git a/nuxt.config.ts b/nuxt.config.ts index 4a420a154d..c0938e0be4 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -5,6 +5,7 @@ import { resolve, basename } from 'pathe' import { defineNuxtConfig } from 'nuxt/config' import { globIterate } from 'glob' import { match as matchLocale } from '@formatjs/intl-localematcher' +import { getStorageMounts } from './config/storages.ts' const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/' const STAGING_ARIADNE_URL = 'https://staging-ariadne.modrinth.com/v1/' @@ -232,6 +233,7 @@ export default defineNuxtConfig({ parserless: 'only-prod', }, nitro: { + storage: getStorageMounts(), moduleSideEffects: ['@vintl/compact-number/locale-data'], }, }) diff --git a/server/storage/cached-cloudflare-kv-binding.ts b/server/storage/cached-cloudflare-kv-binding.ts new file mode 100644 index 0000000000..b961fb199f --- /dev/null +++ b/server/storage/cached-cloudflare-kv-binding.ts @@ -0,0 +1,7 @@ +import cloudflareKVStorage, { KVOptions } from 'unstorage/drivers/cloudflare-kv-binding' +import { Driver } from 'unstorage' +import cachedDriver from './cached.ts' + +export default function cachedVercelKV(opts: KVOptions): Driver { + return cachedDriver({ driver: cloudflareKVStorage(opts) }) +} diff --git a/server/storage/cached-vercel-kv.ts b/server/storage/cached-vercel-kv.ts new file mode 100644 index 0000000000..c6371443dc --- /dev/null +++ b/server/storage/cached-vercel-kv.ts @@ -0,0 +1,7 @@ +import vercelStorage, { VercelKVOptions } from 'unstorage/drivers/vercel-kv' +import { Driver } from 'unstorage' +import cachedDriver from './cached.ts' + +export default function cachedVercelKV(opts: VercelKVOptions): Driver { + return cachedDriver({ driver: vercelStorage(opts) }) +} diff --git a/server/storage/cached.ts b/server/storage/cached.ts new file mode 100644 index 0000000000..21491568f8 --- /dev/null +++ b/server/storage/cached.ts @@ -0,0 +1,31 @@ +import { Driver } from 'unstorage' +import memoryDriver from 'unstorage/drivers/memory' + +export interface CachedOptions { + driver: Driver +} + +export default function cached(options: CachedOptions): Driver { + const { driver } = options + const memory = memoryDriver() as Driver + return { + ...driver, + name: driver.name ? `cached:${driver.name}` : `cached`, + options, + async hasItem(key) { + return (await memory.hasItem(key, {})) || (await driver.hasItem(key, {})) + }, + async getItem(key) { + const memoryLookup = await memory.getItem(key) + if (memoryLookup !== null) return memoryLookup + + const lookup = await driver.getItem(key) + memory.setItem!(key, lookup as any, {}) + return lookup + }, + async setItem(key, value) { + memory.setItem!(key, value, {}) + await driver.setItem?.(key, value, {}) + }, + } +}