mirror of
https://github.com/SukkaW/Surge.git
synced 2025-12-12 01:00:34 +08:00
Feat: new fs memo cache
This commit is contained in:
parent
5aee1b6870
commit
489d73ec86
@ -16,12 +16,6 @@ import { Custom304NotModifiedError, CustomAbortError, CustomNoETagFallbackError,
|
|||||||
import type { IncomingHttpHeaders } from 'undici/types/header';
|
import type { IncomingHttpHeaders } from 'undici/types/header';
|
||||||
import { Headers } from 'undici';
|
import { Headers } from 'undici';
|
||||||
|
|
||||||
const enum CacheStatus {
|
|
||||||
Hit = 'hit',
|
|
||||||
Stale = 'stale',
|
|
||||||
Miss = 'miss'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CacheOptions<S = string> {
|
export interface CacheOptions<S = string> {
|
||||||
/** Path to sqlite file dir */
|
/** Path to sqlite file dir */
|
||||||
cachePath?: string,
|
cachePath?: string,
|
||||||
@ -34,6 +28,7 @@ export interface CacheOptions<S = string> {
|
|||||||
|
|
||||||
interface CacheApplyRawOption {
|
interface CacheApplyRawOption {
|
||||||
ttl?: number | null,
|
ttl?: number | null,
|
||||||
|
cacheName?: string,
|
||||||
temporaryBypass?: boolean,
|
temporaryBypass?: boolean,
|
||||||
incrementTtlWhenHit?: boolean
|
incrementTtlWhenHit?: boolean
|
||||||
}
|
}
|
||||||
@ -43,7 +38,7 @@ interface CacheApplyNonRawOption<T, S> extends CacheApplyRawOption {
|
|||||||
deserializer: (cached: S) => T
|
deserializer: (cached: S) => T
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheApplyOption<T, S> = T extends S ? CacheApplyRawOption : CacheApplyNonRawOption<T, S>;
|
export type CacheApplyOption<T, S> = T extends S ? CacheApplyRawOption : CacheApplyNonRawOption<T, S>;
|
||||||
|
|
||||||
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
|
||||||
@ -164,23 +159,27 @@ export class Cache<S = string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: string, defaultValue?: S): S | undefined {
|
get(key: string): S | null {
|
||||||
const rv = this.db.prepare<string, { value: S }>(
|
const rv = this.db.prepare<string, { value: S, ttl: number }>(
|
||||||
`SELECT value FROM ${this.tableName} WHERE key = ? LIMIT 1`
|
`SELECT ttl, value FROM ${this.tableName} WHERE key = ? LIMIT 1`
|
||||||
).get(key);
|
).get(key);
|
||||||
|
|
||||||
if (!rv) return defaultValue;
|
if (!rv) return null;
|
||||||
|
|
||||||
|
if (rv.ttl < Date.now()) {
|
||||||
|
this.del(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rv.value == null) {
|
||||||
|
this.del(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return rv.value;
|
return rv.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
has(key: string): CacheStatus {
|
updateTtl(key: string, ttl: number): void {
|
||||||
const now = Date.now();
|
|
||||||
const rv = this.db.prepare<string, { ttl: number }>(`SELECT ttl FROM ${this.tableName} WHERE key = ?`).get(key);
|
|
||||||
|
|
||||||
return rv ? (rv.ttl > now ? CacheStatus.Hit : CacheStatus.Stale) : CacheStatus.Miss;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateTtl(key: string, ttl: number): void {
|
|
||||||
this.db.prepare(`UPDATE ${this.tableName} SET ttl = ? WHERE key = ?;`).run(Date.now() + ttl, key);
|
this.db.prepare(`UPDATE ${this.tableName} SET ttl = ? WHERE key = ?;`).run(Date.now() + ttl, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +192,7 @@ export class Cache<S = string> {
|
|||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
opt: CacheApplyOption<T, S>
|
opt: CacheApplyOption<T, S>
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const { ttl, temporaryBypass, incrementTtlWhenHit } = opt;
|
const { ttl, temporaryBypass, incrementTtlWhenHit, cacheName } = opt;
|
||||||
|
|
||||||
if (temporaryBypass) {
|
if (temporaryBypass) {
|
||||||
return fn();
|
return fn();
|
||||||
@ -205,7 +204,7 @@ export class Cache<S = string> {
|
|||||||
|
|
||||||
const cached = this.get(key);
|
const cached = this.get(key);
|
||||||
if (cached == null) {
|
if (cached == null) {
|
||||||
console.log(picocolors.yellow('[cache] miss'), picocolors.gray(key), picocolors.gray(`ttl: ${TTL.humanReadable(ttl)}`));
|
console.log(picocolors.yellow('[cache] miss'), picocolors.gray(cacheName || key), picocolors.gray(`ttl: ${TTL.humanReadable(ttl)}`));
|
||||||
|
|
||||||
const serializer = 'serializer' in opt ? opt.serializer : identity as any;
|
const serializer = 'serializer' in opt ? opt.serializer : identity as any;
|
||||||
|
|
||||||
@ -217,7 +216,7 @@ export class Cache<S = string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(picocolors.green('[cache] hit'), picocolors.gray(key));
|
console.log(picocolors.green('[cache] hit'), picocolors.gray(cacheName || key));
|
||||||
|
|
||||||
if (incrementTtlWhenHit) {
|
if (incrementTtlWhenHit) {
|
||||||
this.updateTtl(key, ttl);
|
this.updateTtl(key, ttl);
|
||||||
|
|||||||
51
Build/lib/fs-memo.ts
Normal file
51
Build/lib/fs-memo.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { Cache } from './cache-filesystem';
|
||||||
|
import type { CacheApplyOption } from './cache-filesystem';
|
||||||
|
import { isCI } from 'ci-info';
|
||||||
|
|
||||||
|
const fsMemoCache = new Cache({ cachePath: path.resolve(__dirname, '../../.cache') });
|
||||||
|
|
||||||
|
const TTL = isCI
|
||||||
|
// We run CI daily, so 1.5 days TTL is enough to persist the cache across runs
|
||||||
|
? 1.5 * 86400 * 1000
|
||||||
|
// We run locally less frequently, so we need to persist the cache for longer, 7 days
|
||||||
|
: 7 * 86400 * 1000;
|
||||||
|
|
||||||
|
type JSONValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| JSONObject
|
||||||
|
| JSONArray;
|
||||||
|
|
||||||
|
interface JSONObject {
|
||||||
|
[key: string]: JSONValue
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JSONArray extends Array<JSONValue> {}
|
||||||
|
|
||||||
|
export function cache<Args extends JSONValue[], T>(
|
||||||
|
cb: (...args: Args) => Promise<T>,
|
||||||
|
opt: Omit<CacheApplyOption<T, string>, 'ttl'>
|
||||||
|
): (...args: Args) => Promise<T> {
|
||||||
|
// TODO if cb.toString() is long we should hash it
|
||||||
|
const fixedKey = cb.toString();
|
||||||
|
|
||||||
|
return async function cachedCb(...args: Args) {
|
||||||
|
// Construct the complete cache key for this function invocation
|
||||||
|
// TODO stringify is limited. For now we uses typescript to guard the args.
|
||||||
|
const cacheKey = `${fixedKey}|${JSON.stringify(args)}`;
|
||||||
|
const cacheName = cb.name || cacheKey;
|
||||||
|
|
||||||
|
return fsMemoCache.apply(
|
||||||
|
cacheKey,
|
||||||
|
cb,
|
||||||
|
{
|
||||||
|
cacheName,
|
||||||
|
...opt,
|
||||||
|
ttl: TTL
|
||||||
|
} as CacheApplyOption<T, string>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user