Skip to content

Commit 13661ea

Browse files
committed
add ETag to skipSegments byHash
1 parent 3a0de29 commit 13661ea

File tree

6 files changed

+71
-2
lines changed

6 files changed

+71
-2
lines changed

src/app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,21 @@ import { getVideoLabelsByHash } from "./routes/getVideoLabelByHash";
5050
import { addFeature } from "./routes/addFeature";
5151
import { generateTokenRequest } from "./routes/generateToken";
5252
import { verifyTokenRequest } from "./routes/verifyToken";
53+
import { cacheMiddlware } from "./middleware/etag";
5354

5455
export function createServer(callback: () => void): Server {
5556
// Create a service (the app object is just a callback).
5657
const app = express();
5758

5859
const router = ExpressPromiseRouter();
5960
app.use(router);
61+
app.set("etag", false); // disable built in etag
6062

6163
//setup CORS correctly
6264
router.use(corsMiddleware);
6365
router.use(loggerMiddleware);
6466
router.use("/api/", apiCspMiddleware);
67+
router.use(cacheMiddlware);
6568
router.use(express.json());
6669

6770
if (config.userCounterURL) router.use(userCounter);

src/middleware/cors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import { NextFunction, Request, Response } from "express";
33
export function corsMiddleware(req: Request, res: Response, next: NextFunction): void {
44
res.header("Access-Control-Allow-Origin", "*");
55
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE");
6-
res.header("Access-Control-Allow-Headers", "Content-Type");
6+
res.header("Access-Control-Allow-Headers", "Content-Type, If-None-Match");
77
next();
88
}

src/middleware/etag.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NextFunction, Request, Response } from "express";
2+
import { VideoID, VideoIDHash, Service } from "../types/segments.model";
3+
import { QueryCacher } from "../utils/queryCacher";
4+
import { skipSegmentsHashKey, skipSegmentsKey, videoLabelsHashKey, videoLabelsKey } from "../utils/redisKeys";
5+
6+
type hashType = "skipSegments" | "skipSegmentsHash" | "videoLabel" | "videoLabelHash";
7+
type ETag = `${hashType};${VideoIDHash};${Service};${number}`;
8+
9+
export function cacheMiddlware(req: Request, res: Response, next: NextFunction): void {
10+
const reqEtag = req.get("If-None-Match") as string;
11+
// if weak etag, do not handle
12+
if (!reqEtag || reqEtag.startsWith("W/")) return next();
13+
// split into components
14+
const [hashType, hashKey, service, lastModified] = reqEtag.split(";");
15+
// fetch last-modified
16+
getLastModified(hashType as hashType, hashKey as VideoIDHash, service as Service)
17+
.then(redisLastModified => {
18+
if (redisLastModified <= new Date(Number(lastModified) + 1000)) {
19+
// match cache, generate etag
20+
const etag = `${hashType};${hashKey};${service};${redisLastModified.getTime()}` as ETag;
21+
res.status(304).set("etag", etag).send();
22+
}
23+
else next();
24+
})
25+
.catch(next);
26+
}
27+
28+
function getLastModified(hashType: hashType, hashKey: (VideoID | VideoIDHash), service: Service): Promise<Date | null> {
29+
let redisKey: string | null;
30+
if (hashType === "skipSegments") redisKey = skipSegmentsKey(hashKey as VideoID, service);
31+
else if (hashType === "skipSegmentsHash") redisKey = skipSegmentsHashKey(hashKey as VideoIDHash, service);
32+
else if (hashType === "videoLabel") redisKey = videoLabelsKey(hashKey as VideoID, service);
33+
else if (hashType === "videoLabelHash") redisKey = videoLabelsHashKey(hashKey as VideoIDHash, service);
34+
else return Promise.reject();
35+
return QueryCacher.getKeyLastModified(redisKey);
36+
}
37+
38+
export async function getEtag(hashType: hashType, hashKey: VideoIDHash, service: Service): Promise<ETag> {
39+
const lastModified = await getLastModified(hashType, hashKey, service);
40+
return `${hashType};${hashKey};${service};${lastModified.getTime()}` as ETag;
41+
}
42+
43+
/* example usage
44+
import { getEtag } from "../middleware/etag";
45+
await getEtag(hashType, hashPrefix, service)
46+
.then(etag => res.set("ETag", etag))
47+
.catch(() => null);
48+
*/

src/routes/getSkipSegmentsByHash.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Request, Response } from "express";
44
import { ActionType, Category, SegmentUUID, VideoIDHash, Service } from "../types/segments.model";
55
import { getService } from "../utils/getService";
66
import { Logger } from "../utils/logger";
7+
import { getEtag } from "../middleware/etag";
78

89
export async function getSkipSegmentsByHash(req: Request, res: Response): Promise<Response> {
910
let hashPrefix = req.params.prefix as VideoIDHash;
@@ -69,6 +70,9 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
6970
const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service);
7071

7172
try {
73+
await getEtag("skipSegmentsHash", hashPrefix, service)
74+
.then(etag => res.set("ETag", etag))
75+
.catch(() => null);
7276
const output = Object.entries(segments).map(([videoID, data]) => ({
7377
videoID,
7478
segments: data.segments,

src/utils/queryCacher.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoID
8787
}
8888
}
8989

90+
async function getKeyLastModified(key: string): Promise<Date> {
91+
if (!config.redis?.enabled) return Promise.reject("ETag - Redis not enabled");
92+
return await redis.ttl(key)
93+
.then(ttl => {
94+
const sinceLive = config.redis?.expiryTime - ttl;
95+
const now = Math.floor(Date.now() / 1000);
96+
return new Date((now-sinceLive) * 1000);
97+
})
98+
.catch(() => Promise.reject("ETag - Redis error"));
99+
}
100+
90101
function clearRatingCache(videoInfo: { hashedVideoID: VideoIDHash; service: Service;}): void {
91102
if (videoInfo) {
92103
redis.del(ratingHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err));
@@ -101,6 +112,7 @@ export const QueryCacher = {
101112
get,
102113
getAndSplit,
103114
clearSegmentCache,
115+
getKeyLastModified,
104116
clearRatingCache,
105-
clearFeatureCache
117+
clearFeatureCache,
106118
};

src/utils/redis.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface RedisSB {
1919
del(...keys: [RedisCommandArgument]): Promise<number>;
2020
increment?(key: RedisCommandArgument): Promise<RedisCommandRawReply[]>;
2121
sendCommand(args: RedisCommandArguments, options?: RedisClientOptions): Promise<RedisReply>;
22+
ttl(key: RedisCommandArgument): Promise<number>;
2223
quit(): Promise<void>;
2324
}
2425

@@ -30,6 +31,7 @@ let exportClient: RedisSB = {
3031
increment: () => new Promise((resolve) => resolve(null)),
3132
sendCommand: () => new Promise((resolve) => resolve(null)),
3233
quit: () => new Promise((resolve) => resolve(null)),
34+
ttl: () => new Promise((resolve) => resolve(null)),
3335
};
3436

3537
let lastClientFail = 0;

0 commit comments

Comments
 (0)