From 787db73a36fad7930a66efc4ebc6877e8e69a3a1 Mon Sep 17 00:00:00 2001 From: Samuel Jensen <44519206+nichtsam@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:36:00 +0800 Subject: [PATCH] cache --- app/utils/cache.server.ts | 100 +++++++++- app/utils/mdx/compile-mdx.server.ts | 5 +- drizzle.config.ts | 2 +- drizzle/0001_fat_monster_badoon.sql | 5 + drizzle/cache.ts | 7 + drizzle/meta/0001_snapshot.json | 292 ++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 2 +- pnpm-lock.yaml | 17 +- 9 files changed, 416 insertions(+), 21 deletions(-) create mode 100644 drizzle/0001_fat_monster_badoon.sql create mode 100644 drizzle/cache.ts create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/app/utils/cache.server.ts b/app/utils/cache.server.ts index 2cd0a34..530001b 100644 --- a/app/utils/cache.server.ts +++ b/app/utils/cache.server.ts @@ -1,10 +1,100 @@ -import { type CacheEntry, lruCacheAdapter } from '@epic-web/cachified' +import { + cachified as baseCachified, + totalTtl, + type CacheEntry, + type CachifiedOptions, + type Cache, +} from '@epic-web/cachified' import { remember } from '@epic-web/remember' +import { eq } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/libsql' import { LRUCache } from 'lru-cache' +import { z } from 'zod' +import * as cacheDbSchema from '#drizzle/cache.ts' +import { env } from './env.server' -const lru = remember( - 'lru-cache', - () => new LRUCache>({ max: 5000 }), +export { longLivedCache, shortLivedCache, cachified } + +const cacheEntrySchema = z.object({ + metadata: z.object({ + createdTime: z.number(), + ttl: z.number().nullable().optional(), + swr: z.number().nullable().optional(), + }), + value: z.custom<{}>((val) => !!val), +}) + +const cacheDb = remember('cacheDb', () => { + const db = drizzle({ + schema: cacheDbSchema, + connection: { + url: env.TURSO_DB_URL, + authToken: env.TURSO_DB_AUTH_TOKEN, + }, + }) + + return db +}) + +const cacheMemory = remember( + 'cacheMemory', + () => new LRUCache({ max: 1000 }), ) -export const lruCache = lruCacheAdapter(lru) +const longLivedCache: Cache = { + async get(key) { + const raw = await cacheDb.query.cacheTable.findFirst({ + where: (cache) => eq(cache.key, key), + }) + + if (!raw) { + return null + } + + const parsed = cacheEntrySchema.safeParse({ + metadata: JSON.parse(raw.metadata), + value: JSON.parse(raw.value), + }) + + if (!parsed.success) { + return null + } + + return parsed.data + }, + set(key, cacheEnrtry) { + void cacheDb + .insert(cacheDbSchema.cacheTable) + .values({ + key, + metadata: JSON.stringify(cacheEnrtry.metadata), + value: JSON.stringify(cacheEnrtry.value), + }) + .then() + }, + delete(key) { + void cacheDb + .delete(cacheDbSchema.cacheTable) + .where(eq(cacheDbSchema.cacheTable.key, key)) + }, +} + +const shortLivedCache: Cache = { + set(key, value) { + const ttl = totalTtl(value?.metadata) + return cacheMemory.set(key, value, { + ttl: ttl === Infinity ? undefined : ttl, + start: value?.metadata?.createdTime, + }) + }, + get(key) { + return cacheMemory.get(key) + }, + delete(key) { + return cacheMemory.delete(key) + }, +} + +function cachified(options: CachifiedOptions): Promise { + return baseCachified(options) +} diff --git a/app/utils/mdx/compile-mdx.server.ts b/app/utils/mdx/compile-mdx.server.ts index a7e5226..55c034c 100644 --- a/app/utils/mdx/compile-mdx.server.ts +++ b/app/utils/mdx/compile-mdx.server.ts @@ -1,8 +1,7 @@ -import { cachified } from '@epic-web/cachified' import { remember } from '@epic-web/remember' import { bundleMDX as _bundleMDX } from 'mdx-bundler' import PQueue from 'p-queue' -import { lruCache } from '../cache.server.ts' +import { cachified, longLivedCache } from '../cache.server.ts' import { type MdxBundleSource } from './mdx.server.ts' async function bundleMDX({ source, files }: MdxBundleSource) { @@ -44,7 +43,7 @@ const cachedBundleMDX = ({ const key = `${slug}:compiled` const compileMdx = cachified({ key, - cache: lruCache, + cache: longLivedCache, getFreshValue: () => queuedBundleMDX(bundle), }) diff --git a/drizzle.config.ts b/drizzle.config.ts index 1961696..81d254e 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -11,7 +11,7 @@ const env = z export default { dialect: 'turso', - schema: './drizzle/schema.ts', + schema: ['./drizzle/schema.ts', './drizzle/cache.ts'], out: './drizzle', dbCredentials: { url: env.TURSO_DB_URL, diff --git a/drizzle/0001_fat_monster_badoon.sql b/drizzle/0001_fat_monster_badoon.sql new file mode 100644 index 0000000..6d6c17c --- /dev/null +++ b/drizzle/0001_fat_monster_badoon.sql @@ -0,0 +1,5 @@ +CREATE TABLE `cache` ( + `key` text PRIMARY KEY NOT NULL, + `metadata` text NOT NULL, + `value` text NOT NULL +); diff --git a/drizzle/cache.ts b/drizzle/cache.ts new file mode 100644 index 0000000..ddb292d --- /dev/null +++ b/drizzle/cache.ts @@ -0,0 +1,7 @@ +import { sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const cacheTable = sqliteTable('cache', { + key: text('key').primaryKey(), + metadata: text('metadata').notNull(), + value: text('value').notNull(), +}) diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..ee000c2 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,292 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f3513090-55ad-4eb0-991c-8c366f8261d8", + "prevId": "ff1a202c-b789-43f8-a8a2-008a199fe05c", + "tables": { + "connection": { + "name": "connection", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "connection_provider_name_provider_id_unique": { + "name": "connection_provider_name_provider_id_unique", + "columns": [ + "provider_name", + "provider_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "connection_user_id_user_id_fk": { + "name": "connection_user_id_user_id_fk", + "tableFrom": "connection", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expiration_at": { + "name": "expiration_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_image": { + "name": "user_image", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blob": { + "name": "blob", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_image_user_id_user_id_fk": { + "name": "user_image_user_id_user_id_fk", + "tableFrom": "user_image", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cache": { + "name": "cache", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6a39d0d..1ae4b65 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1706284684107, "tag": "0000_busy_flatman", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1733131535859, + "tag": "0001_fat_monster_badoon", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 3ea4319..3480f62 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "dependencies": { "@conform-to/react": "1.2.2", "@conform-to/zod": "1.2.2", - "@epic-web/cachified": "4.0.0", + "@epic-web/cachified": "5.2.0", "@epic-web/config": "1.16.3", "@epic-web/remember": "1.1.0", "@libsql/client": "0.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b7e02e..bc5c9b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: 1.2.2 version: 1.2.2(zod@3.23.8) '@epic-web/cachified': - specifier: 4.0.0 - version: 4.0.0 + specifier: 5.2.0 + version: 5.2.0 '@epic-web/config': specifier: 1.16.3 version: 1.16.3(@testing-library/dom@10.4.0)(@typescript-eslint/utils@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(prettier@3.3.3)(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.1)(jsdom@25.0.1)) @@ -582,11 +582,8 @@ packages: '@emotion/hash@0.9.1': resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} - '@epic-web/cachified@4.0.0': - resolution: {integrity: sha512-5XPdddRV1NrRev5HjfvM72tBh2jnfzBXdbcirtt0G0hveDHT4pOPrYCfyHNt+QcKSX0yifXimWMirOVXtg8ewQ==} - - '@epic-web/cachified@5.1.2': - resolution: {integrity: sha512-8Q1J/jF0bOKUN+XPTSUo+z34WJHMBLkuDv+HkPPS9ufs+cHvInWrLOLi3qIljhe03Xq777pwEDBKnMafetnPhA==} + '@epic-web/cachified@5.2.0': + resolution: {integrity: sha512-E/2LdIhS/wcn3ykV+u5xbTFahKtzReO0k4/cVtE7KBYiLCgR6bPRmlDUuyfWhbZX3zOmK+6OL7qdHkvgCWwSiA==} '@epic-web/config@1.16.3': resolution: {integrity: sha512-RxcokzX6bhVLiyiJD5cWTjt8mi75Os0Jut+7h9X0zr6sT22SUG0hFPDXHwQbTqSdugFBxXCfb03nvCCgHvryOw==} @@ -7495,9 +7492,7 @@ snapshots: '@emotion/hash@0.9.1': {} - '@epic-web/cachified@4.0.0': {} - - '@epic-web/cachified@5.1.2': {} + '@epic-web/cachified@5.2.0': {} '@epic-web/config@1.16.3(@testing-library/dom@10.4.0)(@typescript-eslint/utils@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(prettier@3.3.3)(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.1)(jsdom@25.0.1))': dependencies: @@ -9415,7 +9410,7 @@ snapshots: '@sly-cli/sly@1.14.0(typescript@5.6.3)': dependencies: - '@epic-web/cachified': 5.1.2 + '@epic-web/cachified': 5.2.0 chalk: 5.3.0 commander: 11.1.0 compare-versions: 6.1.0