Files
web/apps/redis-api/src/routes/api/cache.ts
Joakim Jäderberg 60f4b8d878 Merged in fix/redis-api-package-mismatch (pull request #3278)
Fix/redis api package mismatch

* Migrate scandic-web

* Migrate partner-sas

* Enable any rule in partner-sas

* fix: redis-api package version mismatch

* Merge branch 'master' of bitbucket.org:scandic-swap/web into fix/redis-api-package-mismatch


Approved-by: Anton Gunnarsson
2025-12-03 09:54:30 +00:00

161 lines
5.1 KiB
TypeScript

import * as Sentry from "@sentry/bun";
import { Elysia, t } from "elysia";
import { redis } from "@/services/redis";
import { queueDelete, queueDeleteMultiple } from "@/services/redis/queueDelete";
import { loggerModule } from "@/utils/logger";
import { truncate } from "@/utils/truncate";
import { validateKey } from "@/utils/validateKey";
const QUERY_TYPE = t.Object({ key: t.String({}) });
const DELETEMULTIPLE_BODY_TYPE = t.Object({
keys: t.Array(t.String()),
fuzzy: t.Optional(t.Boolean({ default: false })),
});
const cacheRouteLogger = loggerModule("cacheRoute");
export const cacheRoutes = new Elysia({ prefix: "/cache" })
.get(
"/",
async ({ query: { key }, status }) => {
key = validateKey(key);
cacheRouteLogger.debug(`GET /cache ${key}`);
const value = await redis.get(key);
if (!value) {
return status("Not Found", "Not Found");
}
try {
const output = JSON.parse(value);
return { data: output };
} catch (e) {
redis.unlink(key);
cacheRouteLogger.error(`Invalid JSON in cache for '${key}'`, e);
Sentry.captureException(e, {
tags: { cacheKey: key },
extra: { cacheKey: key, value: `${truncate(value, 100)}` },
fingerprint: ["get-route_invalid-json-in-cache"],
});
return status("Not Found", "Not Found");
}
},
{
query: QUERY_TYPE,
response: { 200: t.Object({ data: t.Any() }), 404: t.String() },
},
)
.put(
"/",
async ({ query: { key }, body, status, set }) => {
key = validateKey(key);
cacheRouteLogger.debug(`PUT /cache ${key}`);
if (!body.ttl || body.ttl < 0) {
cacheRouteLogger.warn(
`PUT /cache ${key} with ttl=${body.ttl}, will not cache the data`,
);
return status("Bad Request", "ttl is required");
}
await redis.set(key, JSON.stringify(body.data), "EX", body.ttl);
set.status = 204;
return undefined;
},
{
body: t.Object({ data: t.Any(), ttl: t.Number() }),
query: QUERY_TYPE,
response: { 204: t.Undefined(), 400: t.String() },
},
)
.delete(
"/multiple",
async ({ body: { keys, fuzzy = false } }) => {
const validatedKeys = keys.filter((x) => !!x).map(validateKey);
if (validatedKeys.length === 0) {
return { deletedKeys: 0 };
}
cacheRouteLogger.debug(
`DELETE /multiple keys=${validatedKeys.join(",")} ${fuzzy ? "(fuzzy)" : ""}`,
);
// 1. Fuzzy deletes → Single SCAN pass
if (fuzzy) {
const patterns = validatedKeys.map((k) => `*${k}*`);
await queueDeleteMultiple({ patterns });
return { status: "queued" };
}
// 2. Exact deletes → Batch unlink
const now = performance.now();
// Use UNLINK for async deletes
const deletedKeys = await redis.unlink(...validatedKeys);
const elapsed = performance.now() - now;
cacheRouteLogger.info(
`Deleted ${deletedKeys} keys in ${elapsed}ms`,
{
deletedKeys,
keys: validatedKeys,
elapsed,
},
);
return { deletedKeys };
},
{
body: DELETEMULTIPLE_BODY_TYPE,
response: {
200: t.Union([
t.Object({ deletedKeys: t.Number() }),
t.Object({ status: t.Literal("queued") }),
]),
400: t.String(),
},
},
)
.delete(
"/",
async ({ query: { key, fuzzy } }) => {
key = validateKey(key);
cacheRouteLogger.debug(
`DELETE /cache ${key} ${fuzzy ? "fuzzy" : ""}`,
);
if (fuzzy) {
await queueDelete({ pattern: `*${key}*` });
return { status: "queued" };
}
const now = performance.now();
const deletedKeys = await redis.unlink(key);
const elapsed = performance.now() - now;
cacheRouteLogger.info(
`Deleted ${deletedKeys} keys for '${key}' in ${elapsed}ms`,
{ fuzzy, deletedKeys, key, elapsed },
);
return { deletedKeys };
},
{
query: t.Object({
...QUERY_TYPE.properties,
...t.Object({ fuzzy: t.Optional(t.Boolean()) }).properties,
}),
response: {
200: t.Union([
t.Object({ deletedKeys: t.Number() }),
t.Object({ status: t.Literal("queued") }),
]),
400: t.String(),
},
},
);