diff --git a/bun.lock b/bun.lock index 079ae12..825ecf2 100644 --- a/bun.lock +++ b/bun.lock @@ -7,8 +7,7 @@ "dependencies": { "@libsql/client": "0.17.3", "bcryptjs": "3.0.3", - "croner": "^10.0.1", - "croner": "^10.0.1", + "croner": "10.0.1", "dotenv": "17.4.1", "drizzle-orm": "0.45.2", "drizzle-pkg": "workspace:*", @@ -35,6 +34,16 @@ "typescript": "6.0.2", }, }, + "packages/cache": { + "name": "cache", + "version": "0.1.0", + "dependencies": { + "ioredis": "^5.10.1", + }, + "devDependencies": { + "@types/node": "20.0.0", + }, + }, "packages/drizzle-pkg": { "name": "drizzle-pkg", }, @@ -692,6 +701,8 @@ "cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "cache": ["cache@workspace:packages/cache"], + "caniuse-api": ["caniuse-api@3.0.0", "https://registry.npmmirror.com/caniuse-api/-/caniuse-api-3.0.0.tgz", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], "caniuse-lite": ["caniuse-lite@1.0.30001787", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], @@ -1744,6 +1755,8 @@ "c12/pkg-types": ["pkg-types@2.3.0", "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "cache/@types/node": ["@types/node@20.0.0", "", {}, "sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw=="], + "compress-commons/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "compress-commons/readable-stream": ["readable-stream@4.7.0", "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], diff --git a/packages/cache/index.ts b/packages/cache/index.ts new file mode 100644 index 0000000..09ebb57 --- /dev/null +++ b/packages/cache/index.ts @@ -0,0 +1,35 @@ +import { CacheManager } from './lib/managers/cache-manager' +import { MemoryDriver } from './lib/drivers/memory-driver' +import { RedisDriver } from './lib/drivers/redis-driver' +import type { CacheDriver, CacheManagerOptions } from './lib/types' +import type { RedisDriverOptions } from './lib/drivers/redis-driver' + +export * from './lib/types' +export { MemoryDriver } from './lib/drivers/memory-driver' +export { RedisDriver } from './lib/drivers/redis-driver' +export { CacheManager } from './lib/managers/cache-manager' +export type { RedisDriverOptions } from './lib/drivers/redis-driver' + +export interface CacheFactoryOptions { + redis?: RedisDriverOptions + memory?: boolean + defaultTtl?: number +} + +export function createCache(options: CacheFactoryOptions): CacheManager { + const drivers: CacheDriver[] = [] + + if (options.redis) { + drivers.push(new RedisDriver(options.redis)) + } + + if (options.memory !== false) { + drivers.push(new MemoryDriver()) + } + + if (drivers.length === 0) { + throw new Error('[cache] at least one driver (redis or memory) is required') + } + + return new CacheManager({ drivers, defaultTtl: options.defaultTtl }) +} \ No newline at end of file diff --git a/packages/cache/lib/drivers/memory-driver.ts b/packages/cache/lib/drivers/memory-driver.ts new file mode 100644 index 0000000..e14448f --- /dev/null +++ b/packages/cache/lib/drivers/memory-driver.ts @@ -0,0 +1,48 @@ +import type { CacheDriver } from '../types' + +interface MemoryEntry { + value: unknown + expiresAt: number | null +} + +export class MemoryDriver implements CacheDriver { + name = 'memory' + private store = new Map() + + async get(key: string): Promise { + const entry = this.store.get(key) + if (!entry) return null + + if (entry.expiresAt !== null && entry.expiresAt < Date.now()) { + this.store.delete(key) + return null + } + + return entry.value as T + } + + async set(key: string, value: T, ttl = 0): Promise { + const expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : null + this.store.set(key, { value, expiresAt }) + } + + async del(key: string): Promise { + this.store.delete(key) + } + + async exists(key: string): Promise { + const entry = this.store.get(key) + if (!entry) return false + + if (entry.expiresAt !== null && entry.expiresAt < Date.now()) { + this.store.delete(key) + return false + } + + return true + } + + async clear(): Promise { + this.store.clear() + } +} \ No newline at end of file diff --git a/packages/cache/lib/drivers/redis-driver.ts b/packages/cache/lib/drivers/redis-driver.ts new file mode 100644 index 0000000..3934900 --- /dev/null +++ b/packages/cache/lib/drivers/redis-driver.ts @@ -0,0 +1,88 @@ +import Redis from 'ioredis' +import type { CacheDriver } from '../types' + +export interface RedisDriverOptions { + host: string + port: number + password?: string + db?: number +} + +export class RedisDriver implements CacheDriver { + name = 'redis' + private redis: Redis + + constructor(options: RedisDriverOptions) { + this.redis = new Redis({ + host: options.host, + port: options.port, + password: options.password, + db: options.db ?? 0, + lazyConnect: true, + }) + } + + private serialize(value: T): string { + return JSON.stringify(value) + } + + private deserialize(data: string): T { + try { + return JSON.parse(data) as T + } catch { + throw new Error(`[cache] Failed to deserialize data: ${data.substring(0, 100)}`) + } + } + + async get(key: string): Promise { + try { + const data = await this.redis.get(key) + if (!data) return null + return this.deserialize(data) + } catch (err) { + throw err + } + } + + async set(key: string, value: T, ttl = 0): Promise { + try { + const serialized = this.serialize(value) + if (ttl > 0) { + await this.redis.set(key, serialized, 'EX', ttl) + } else { + await this.redis.set(key, serialized) + } + } catch (err) { + throw err + } + } + + async del(key: string): Promise { + try { + await this.redis.del(key) + } catch (err) { + throw err + } + } + + async exists(key: string): Promise { + try { + const result = await this.redis.exists(key) + return result === 1 + } catch (err) { + throw err + } + } + + async clear(): Promise { + try { + await this.redis.flushdb() + } catch (err) { + throw err + } + } + + async disconnect(): Promise { + await this.redis.quit() + } +} \ No newline at end of file diff --git a/packages/cache/lib/managers/cache-manager.ts b/packages/cache/lib/managers/cache-manager.ts new file mode 100644 index 0000000..7a431cf --- /dev/null +++ b/packages/cache/lib/managers/cache-manager.ts @@ -0,0 +1,77 @@ +import type { CacheDriver, CacheManagerOptions } from '../types' + +export class CacheManager { + private drivers: CacheDriver[] + private defaultTtl: number + + constructor(options: CacheManagerOptions) { + if (!options.drivers || options.drivers.length === 0) { + throw new Error('[cache] at least one driver is required') + } + this.drivers = options.drivers + this.defaultTtl = options.defaultTtl ?? 0 + } + + async get(key: string): Promise { + for (const driver of this.drivers) { + try { + const value = await driver.get(key) + return value + } catch (err) { + console.warn(`[cache] ${driver.name} get failed:`, err) + continue + } + } + return null + } + + async set(key: string, value: T, ttl?: number): Promise { + const effectiveTtl = ttl ?? this.defaultTtl + await Promise.all( + this.drivers.map(async (driver) => { + try { + await driver.set(key, value, effectiveTtl) + } catch (err) { + console.warn(`[cache] ${driver.name} set failed:`, err) + } + }) + ) + } + + async del(key: string): Promise { + await Promise.all( + this.drivers.map(async (driver) => { + try { + await driver.del(key) + } catch (err) { + console.warn(`[cache] ${driver.name} del failed:`, err) + } + }) + ) + } + + async exists(key: string): Promise { + for (const driver of this.drivers) { + try { + const exists = await driver.exists(key) + if (exists) return true + } catch (err) { + console.warn(`[cache] ${driver.name} exists failed:`, err) + continue + } + } + return false + } + + async clear(): Promise { + await Promise.all( + this.drivers.map(async (driver) => { + try { + await driver.clear() + } catch (err) { + console.warn(`[cache] ${driver.name} clear failed:`, err) + } + }) + ) + } +} \ No newline at end of file diff --git a/packages/cache/lib/types.ts b/packages/cache/lib/types.ts new file mode 100644 index 0000000..94d6040 --- /dev/null +++ b/packages/cache/lib/types.ts @@ -0,0 +1,18 @@ +export interface CacheEntry { + value: T + expiresAt: number | null // Unix ms,null = 永不过期 +} + +export interface CacheDriver { + name: string + get(key: string): Promise + set(key: string, value: T, ttl?: number): Promise + del(key: string): Promise + exists(key: string): Promise + clear(): Promise +} + +export interface CacheManagerOptions { + drivers: CacheDriver[] + defaultTtl?: number +} diff --git a/packages/cache/package.json b/packages/cache/package.json new file mode 100644 index 0000000..3c43c6d --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,14 @@ +{ + "name": "cache", + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "ioredis": "^5.10.1" + }, + "devDependencies": { + "@types/node": "20.0.0" + } +} diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json new file mode 100644 index 0000000..3f3c720 --- /dev/null +++ b/packages/cache/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "index.ts", + "lib/**/*.ts" + ] +} diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index d630149..3f2cb8e 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ