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
161 lines
5.1 KiB
TypeScript
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(),
|
|
},
|
|
},
|
|
);
|