Skip to content

Commit

Permalink
feat(cache): support browser cache or 304
Browse files Browse the repository at this point in the history
  • Loading branch information
jiawei397 committed Feb 11, 2022
1 parent 3e2cf38 commit e40e771
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 18 deletions.
4 changes: 4 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export {
Status,
STATUS_TEXT,
} from "https://deno.land/x/[email protected]/mod.ts";
export {
calculate,
ifNoneMatch,
} from "https://deno.land/x/[email protected]/etag.ts";

export type {
FormDataReadOptions,
Expand Down
1 change: 1 addition & 0 deletions modules/cache/example/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppController } from "./app.controller.ts";
imports: [
CacheModule.register({
ttl: 60,
policy: "public",
}),
],
controllers: [AppController],
Expand Down
2 changes: 2 additions & 0 deletions modules/cache/src/cache.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const optionKey = Symbol("options");
export const META_CACHE_TTL_KEY = Symbol("meta:cache:ttl");

export const META_CACHE_KEY_KEY = Symbol("meta:cache:key");

export const META_CACHE_POLICY_KEY = Symbol("meta:cache:policy");
76 changes: 59 additions & 17 deletions modules/cache/src/cache.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// deno-lint-ignore-file no-explicit-any
import {
calculate,
Context,
ifNoneMatch,
Inject,
Injectable,
NestInterceptor,
Expand All @@ -11,10 +13,11 @@ import {
import { isDebug } from "../../../src/utils.ts";
import {
META_CACHE_KEY_KEY,
META_CACHE_POLICY_KEY,
META_CACHE_TTL_KEY,
optionKey,
} from "./cache.constant.ts";
import { CacheModuleOptions } from "./cache.interface.ts";
import { CacheModuleOptions, CachePolicy } from "./cache.interface.ts";

export function CacheTTL(ttl: number) {
return (_target: any, _methodName: string, descriptor: any) => {
Expand All @@ -28,15 +31,23 @@ export function CacheKey(key: string) {
};
}

export function SetCachePolicy(policy: CachePolicy) {
return (_target: any, _methodName: string, descriptor: any) => {
Reflect.defineMetadata(META_CACHE_POLICY_KEY, policy, descriptor.value);
};
}

@Injectable()
export class CacheInterceptor implements NestInterceptor {
ttl: number;
max: number;
policy: CachePolicy;
constructor(
@Inject(optionKey) private cacheModuleOptions?: CacheModuleOptions,
) {
this.ttl = cacheModuleOptions?.ttl || 5;
this.max = cacheModuleOptions?.max || 100;
this.policy = cacheModuleOptions?.policy || "no-cache";
}

private caches = new Map<string, any>();
Expand All @@ -55,8 +66,23 @@ export class CacheInterceptor implements NestInterceptor {
return result;
}

intercept(
_context: Context,
async checkEtag(context: Context, val: unknown) {
const etag = context.request.headers.get("If-None-Match");
const str = JSON.stringify(val);
const etagOptions = { weak: true };
const actual = await calculate(str, etagOptions);
context.response.headers.set("etag", actual);
context.response.headers.set("Cache-Control", "no-cache");
if (
etag && !await ifNoneMatch(etag, str, etagOptions) // if etag is not match, then will return 200
) {
context.response.status = 304;
}
return val;
}

async intercept(
context: Context,
next: Next,
options: NestInterceptorOptions,
) {
Expand Down Expand Up @@ -84,12 +110,19 @@ export class CacheInterceptor implements NestInterceptor {
options.methodType,
this.joinArgs(options.args),
].join("_"));
console.log(key);
const cacheValue = cache.get(key);
const policy = Reflect.getOwnMetadata(
META_CACHE_POLICY_KEY,
options.target[options.methodName],
) || this.policy;

if (cacheValue !== undefined) {
if (isDebug()) {
console.debug("cache hit", key, cacheValue);
}
if (policy === "no-cache") { // may return 304 when not modified
return this.checkEtag(context, await cacheValue);
}
return cacheValue;
}
const result = next();
Expand All @@ -102,19 +135,28 @@ export class CacheInterceptor implements NestInterceptor {
const st = setTimeout(() => {
cache.delete(key);
}, ttl * 1000);
Promise.resolve(result)
.then((val) => {
if (this.cacheModuleOptions?.isCacheableValue) {
if (!this.cacheModuleOptions.isCacheableValue(val)) {
cache.delete(key);
clearTimeout(st);
}
try {
const val = await result;
if (this.cacheModuleOptions?.isCacheableValue) {
if (!this.cacheModuleOptions.isCacheableValue(val)) {
cache.delete(key);
clearTimeout(st);
return val;
}
})
.catch(() => {
cache.delete(key);
clearTimeout(st);
});
return result;
}
if (policy === "no-cache") {
return this.checkEtag(context, val);
} else {
context.response.headers.set(
"Cache-Control",
policy === "public" ? `max-age=${ttl}` : `${policy}, max-age=${ttl}`,
);
return val;
}
} catch (error) {
cache.delete(key);
clearTimeout(st);
throw error;
}
}
}
15 changes: 15 additions & 0 deletions modules/cache/src/cache.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
// deno-lint-ignore-file no-explicit-any

/**
* If you want the results to be cached directly by the browser, you can set it to public or private.
*
* If you want get the results from the cache, but not to trans it by net, you can set it to no-cache, and the status may be 304 if no modified.
* @see https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control
*/
export type CachePolicy = "public" | "private" | "no-cache";

/**
* Interface defining Cache Manager configuration options.
*
Expand Down Expand Up @@ -29,6 +38,12 @@ export interface CacheManagerOptions {
methodType: string;
args: any[];
}) => string;

/**
* This options will be used to configure the cache-control header.
* @default "no-cache"
*/
policy?: CachePolicy;
}

export interface CacheModuleOptions extends CacheManagerOptions {
Expand Down
4 changes: 3 additions & 1 deletion src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@ export class Router extends OriginRouter {
}

private transResponseResult(context: Context, result: any) {
if (context.response.body === undefined) {
if (context.response.status === 304) {
context.response.body = undefined;
} else if (context.response.body === undefined) {
context.response.body = result;
}
}
Expand Down

0 comments on commit e40e771

Please sign in to comment.