diff --git a/lib/cache.ts b/lib/cache.ts index 4394b98..6b7b3f6 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -6,12 +6,18 @@ interface SetCacheOptions { expires?: number; // Override expiration for individual cache entries } +export const caches = new Map< + string, + { info: () => { count: number; sizeInKB: number } } +>(); + export function createCache( + cacheName: string, createOpts: CreateCacheOptions = {}, ) { const cache = new Map(); - return { + const api = { get(key: string): T | undefined { const entry = cache.get(key); if (!entry) return undefined; @@ -86,4 +92,9 @@ export function createCache( return cache.size; }, }; + + caches.set(cacheName, { + info: api.info.bind(api), + }); + return api; } diff --git a/lib/crud.ts b/lib/crud.ts index 17160cc..4dfb3ea 100644 --- a/lib/crud.ts +++ b/lib/crud.ts @@ -71,7 +71,7 @@ export function createCrud( parse: (doc: string, id: string) => T; }, ) { - const cache = createCache({ expires: 60 * 1000 }); + const cache = createCache(`crud/${prefix}`, { expires: 60 * 1000 }); function pathFromId(id: string) { return `${prefix}${id.replaceAll(":", "")}.md`; diff --git a/lib/documents.ts b/lib/documents.ts index 76bd686..d665bf8 100644 --- a/lib/documents.ts +++ b/lib/documents.ts @@ -10,7 +10,7 @@ import remarkFrontmatter, { } from "https://esm.sh/remark-frontmatter@4.0.1"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; import { fixRenderedMarkdown } from "@lib/helpers.ts"; -import { createLogger } from "@lib/log.ts"; +import { createLogger } from "@lib/log/index.ts"; import { db } from "@lib/db/sqlite.ts"; import { documentTable } from "@lib/db/schema.ts"; import { eq } from "drizzle-orm/sql"; diff --git a/lib/env.ts b/lib/env.ts index 9256375..7379948 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -1,8 +1,6 @@ import path from "node:path"; export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER"); -export const REDIS_HOST = Deno.env.get("REDIS_HOST"); -export const REDIS_PASS = Deno.env.get("REDIS_PASS"); export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY"); export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY"); export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY"); diff --git a/lib/helpers.ts b/lib/helpers.ts index 4437988..1661774 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -9,7 +9,7 @@ export function json(content: unknown) { export const isValidUrl = (urlString: string) => { try { return Boolean(new URL(urlString)); - } catch (e) { + } catch (_e) { return false; } }; @@ -87,6 +87,8 @@ export const createStreamResponse = () => { }; }; +export type StreamResponse = ReturnType; + export function debounce) => void>( this: ThisParameterType, fn: T, diff --git a/lib/image.ts b/lib/image.ts index e1e8951..01ac210 100644 --- a/lib/image.ts +++ b/lib/image.ts @@ -1,5 +1,5 @@ import { rgbToHex } from "@lib/string.ts"; -import { createLogger } from "@lib/log.ts"; +import { createLogger } from "@lib/log/index.ts"; import { generateThumbhash } from "@lib/thumbhash.ts"; import { parseMediaType } from "https://deno.land/std@0.224.0/media_types/parse_media_type.ts"; import path from "node:path"; diff --git a/lib/index.ts b/lib/index.ts deleted file mode 100644 index e1038ae..0000000 --- a/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./documents.ts"; diff --git a/lib/log.ts b/lib/log.ts deleted file mode 100644 index 2f824bd..0000000 --- a/lib/log.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { EventEmitter } from "https://deno.land/x/evtemitter@v3.0.0/mod.ts"; -import { LOG_LEVEL as _LOG_LEVEL } from "@lib/env.ts"; - -enum LOG_LEVEL { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, -} - -const logMap = { - "debug": LOG_LEVEL.DEBUG, - "info": LOG_LEVEL.INFO, - "warn": LOG_LEVEL.WARN, - "error": LOG_LEVEL.ERROR, -} as const; - -const logFuncs = { - [LOG_LEVEL.DEBUG]: console.debug, - [LOG_LEVEL.INFO]: console.info, - [LOG_LEVEL.WARN]: console.warn, - [LOG_LEVEL.ERROR]: console.error, -} as const; - -let longestScope = 0; -let logLevel = (_LOG_LEVEL && _LOG_LEVEL in logMap) - ? logMap[_LOG_LEVEL] - : LOG_LEVEL.DEBUG; - -const ee = new EventEmitter<{ - log: { level: LOG_LEVEL; scope: string; args: unknown[] }; -}>(); - -export function setLogLevel(level: LOG_LEVEL) { - logLevel = level; -} - -type LoggerOptions = { - enabled?: boolean; -}; - -const createLogFunction = - (scope: string, level: LOG_LEVEL) => (...data: unknown[]) => { - ee.emit("log", { level, scope, args: data }); - if (level < logLevel) return; - logFuncs[level](`[${scope.padEnd(longestScope, " ")}]`, ...data); - }; - -export function createLogger(scope: string, _options?: LoggerOptions) { - longestScope = Math.max(scope.length, longestScope); - - return { - debug: createLogFunction(scope, LOG_LEVEL.DEBUG), - info: createLogFunction(scope, LOG_LEVEL.INFO), - error: createLogFunction(scope, LOG_LEVEL.ERROR), - warn: createLogFunction(scope, LOG_LEVEL.WARN), - addEventListener: - ((type, cb) => - ee.addEventListener(type, cb)) as typeof ee.addEventListener, - }; -} diff --git a/lib/log/constants.ts b/lib/log/constants.ts new file mode 100644 index 0000000..6de835a --- /dev/null +++ b/lib/log/constants.ts @@ -0,0 +1,14 @@ +import * as env from "@lib/env.ts"; +import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; +import { join } from "node:path"; +import { getLogLevel, LOG_LEVEL } from "@lib/log/types.ts"; + +export const LOG_DIR = join(env.DATA_DIR, "logs"); + +// Ensure the log directory exists +await ensureDir(LOG_DIR); + +export let logLevel = getLogLevel(env.LOG_LEVEL); +export function setLogLevel(level: LOG_LEVEL) { + logLevel = level; +} diff --git a/lib/logs.ts b/lib/log/fs.ts similarity index 73% rename from lib/logs.ts rename to lib/log/fs.ts index aa107ea..237f091 100644 --- a/lib/logs.ts +++ b/lib/log/fs.ts @@ -1,7 +1,6 @@ -import { createLogger } from "@lib/log.ts"; import { join } from "node:path"; -import { DATA_DIR } from "@lib/env.ts"; -import { ensureDir } from "fs"; +import { LOG_DIR } from "@lib/log/constants.ts"; +import { Log } from "@lib/log/types.ts"; function getLogFileName() { const d = new Date(); @@ -11,30 +10,17 @@ function getLogFileName() { return `${year}-${month}-${day}.log`; } -const LOG_DIR = join(DATA_DIR, "logs"); - -// Ensure the log directory exists -await ensureDir(LOG_DIR); - -const log = createLogger(""); -log.addEventListener("log", async (data) => { - const logEntry = JSON.stringify(data.detail); +export function writeLogEntry(entry: Log) { + const logEntry = JSON.stringify(entry); const logFilePath = join(LOG_DIR, getLogFileName()); // Append the log entry to the file (creating it if it doesn't exist) - await Deno.writeTextFile( + Deno.writeTextFile( logFilePath, new Date().toISOString() + " | " + logEntry + "\n", { append: true }, ); -}); - -export type Log = { - scope: string; - level: number; - date: Date; - args: unknown[]; -}; +} export async function getLogs() { const logFilePath = join(LOG_DIR, getLogFileName()); @@ -59,7 +45,7 @@ export async function getLogs() { // Return the logs sorted by date return logs.sort((a, b) => a.date.getTime() - b.date.getTime()); - } catch (error) { + } catch (_error) { // If file does not exist, return an empty array return []; } diff --git a/lib/log/index.ts b/lib/log/index.ts new file mode 100644 index 0000000..03194e8 --- /dev/null +++ b/lib/log/index.ts @@ -0,0 +1,52 @@ +import { StreamResponse } from "@lib/helpers.ts"; +import { writeLogEntry } from "@lib/log/fs.ts"; +import { LOG_LEVEL, Logger } from "@lib/log/types.ts"; +import { logLevel } from "@lib/log/constants.ts"; + +let longestScope = 0; + +type LoggerOptions = { + enabled?: boolean; +}; + +const createLogFunction = (scope: string, level: LOG_LEVEL) => { + return (...data: unknown[]) => { + writeLogEntry({ level, scope, args: data, date: new Date() }); + if (level < logLevel) return; + const logFunc = { + [LOG_LEVEL.DEBUG]: console.debug, + [LOG_LEVEL.INFO]: console.info, + [LOG_LEVEL.WARN]: console.warn, + [LOG_LEVEL.ERROR]: console.error, + }[level]; + + logFunc(`[${scope.padEnd(longestScope, " ")}]`, ...data); + }; +}; + +export function createLogger(scope: string, _options?: LoggerOptions): Logger { + longestScope = Math.max(scope.length, longestScope); + + return { + debug: createLogFunction(scope, LOG_LEVEL.DEBUG), + info: createLogFunction(scope, LOG_LEVEL.INFO), + error: createLogFunction(scope, LOG_LEVEL.ERROR), + warn: createLogFunction(scope, LOG_LEVEL.WARN), + }; +} + +export function loggerFromStream(stream: StreamResponse) { + return { + debug: (...data: unknown[]) => + stream.enqueue(`${data.length > 1 ? data.join(" ") : data[0]}`), + info: (...data: unknown[]) => + stream.enqueue(`${data.length > 1 ? data.join(" ") : data[0]}`), + error: (...data: unknown[]) => + stream.enqueue(`[ERROR]: ${data.length > 1 ? data.join(" ") : data[0]}`), + warn: (...data: unknown[]) => + stream.enqueue(`[WARN]: ${data.length > 1 ? data.join(" ") : data[0]}`), + }; +} + +export type { Log } from "@lib/log/types.ts"; +export { getLogs } from "@lib/log/fs.ts"; diff --git a/lib/log/types.ts b/lib/log/types.ts new file mode 100644 index 0000000..001cdf0 --- /dev/null +++ b/lib/log/types.ts @@ -0,0 +1,32 @@ +export enum LOG_LEVEL { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, +} +export function getLogLevel(level: string): LOG_LEVEL { + switch (level) { + case "debug": + return LOG_LEVEL.DEBUG; + case "warn": + return LOG_LEVEL.WARN; + case "error": + return LOG_LEVEL.ERROR; + default: + return LOG_LEVEL.INFO; + } +} + +export type Log = { + scope: string; + level: number; + date: Date; + args: unknown[]; +}; + +export interface Logger { + debug: (...data: unknown[]) => void; + info: (...data: unknown[]) => void; + error: (...data: unknown[]) => void; + warn: (...data: unknown[]) => void; +} diff --git a/lib/openai.ts b/lib/openai.ts index 3f86195..2a8a9e6 100644 --- a/lib/openai.ts +++ b/lib/openai.ts @@ -12,7 +12,7 @@ interface MovieRecommendation { title: string; } -const cache = createCache(); +const cache = createCache("movie-recommendations"); function extractListFromResponse(response?: string): string[] { if (!response) return []; diff --git a/lib/recommendation.ts b/lib/recommendation.ts index 7f36045..8dbca87 100644 --- a/lib/recommendation.ts +++ b/lib/recommendation.ts @@ -15,7 +15,7 @@ type RecommendationResource = { year?: number; }; -const cache = createCache(); +const cache = createCache("recommendations"); export async function createRecommendationResource( res: GenericResource, @@ -88,7 +88,7 @@ export async function getSimilarMovies(id: string) { export async function getAllRecommendations(): Promise< RecommendationResource[] > { - const keys = cache.keys("recommendations:movie:*"); + const keys = cache.keys(); const res = await Promise.all(keys.map((k) => cache.get(k))); return res.map((r) => JSON.parse(r)); } diff --git a/lib/tmdb.ts b/lib/tmdb.ts index 8977e47..adcb6bf 100644 --- a/lib/tmdb.ts +++ b/lib/tmdb.ts @@ -10,7 +10,7 @@ import { createCache } from "@lib/cache.ts"; const moviedb = new MovieDb(Deno.env.get("TMDB_API_KEY") || ""); const CACHE_INTERVAL = 1000 * 60 * 24 * 30; -const cache = createCache({ expires: CACHE_INTERVAL }); +const cache = createCache("the-movie-db", { expires: CACHE_INTERVAL }); export const searchMovie = async (query: string, year?: number) => { const id = `query:moviesearch:${query}${year ? `-${year}` : ""}`; diff --git a/lib/webScraper.ts b/lib/webScraper.ts new file mode 100644 index 0000000..a811e82 --- /dev/null +++ b/lib/webScraper.ts @@ -0,0 +1,2 @@ +export function webScrape(url: URL) { +} diff --git a/routes/_app.tsx b/routes/_app.tsx index 9581711..eafd4f7 100644 --- a/routes/_app.tsx +++ b/routes/_app.tsx @@ -5,9 +5,11 @@ export default function App({ Component }: PageProps) { const globalCss = Deno .readTextFileSync("./static/global.css") .replaceAll("\n", ""); + return ( <> + renderMarkdown(`\`\`\`js -${typeof t === "string" ? t : JSON.stringify(t).trim()} +${typeof t === "string" ? t : JSON.stringify(t, null, 2).trim()} \`\`\``); export const handler: Handlers = { @@ -16,11 +16,12 @@ export const handler: Handlers = { if (!("session" in ctx.state)) { throw new AccessDeniedError(); } + console.log({ logs }); return ctx.render({ logs: logs.map((l) => { return { ...l, - html: l.args.map(renderLog).join("\n"), + html: l.args.map(renderLog).join("
"), }; }), }); @@ -34,7 +35,7 @@ function LogLine( ) { return (
@@ -51,13 +52,9 @@ function LogLine(
-
-          {log.html}
-        
-
+ />
); } @@ -67,7 +64,7 @@ export default function Greet( ) { return ( -

Performance

+

Logs

{logs.map((r) => { return ( diff --git a/routes/api/articles/create/index.ts b/routes/api/articles/create/index.ts index bd053eb..a3b6a0b 100644 --- a/routes/api/articles/create/index.ts +++ b/routes/api/articles/create/index.ts @@ -9,7 +9,7 @@ import tds from "https://cdn.skypack.dev/turndown@7.2.0"; import { Article, createArticle } from "@lib/resource/articles.ts"; import { getYoutubeVideoDetails } from "@lib/youtube.ts"; import { extractYoutubeId, isYoutubeLink } from "@lib/string.ts"; -import { createLogger } from "@lib/log.ts"; +import { createLogger } from "@lib/log/index.ts"; const parser = new DOMParser(); diff --git a/routes/api/images/index.ts b/routes/api/images/index.ts index b1d2fac..873e36a 100644 --- a/routes/api/images/index.ts +++ b/routes/api/images/index.ts @@ -1,7 +1,7 @@ import { FreshContext, Handlers } from "$fresh/server.ts"; import { getImageContent } from "@lib/image.ts"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; -import { createLogger } from "@lib/log.ts"; +import { createLogger } from "@lib/log/index.ts"; import { isLocalImage } from "@lib/string.ts"; const log = createLogger("api/image"); diff --git a/routes/api/recipes/create/index.ts b/routes/api/recipes/create/index.ts index 11e0cee..e42862e 100644 --- a/routes/api/recipes/create/index.ts +++ b/routes/api/recipes/create/index.ts @@ -5,7 +5,7 @@ import { AccessDeniedError, BadRequestError } from "@lib/errors.ts"; import { createStreamResponse, isValidUrl } from "@lib/helpers.ts"; import * as openai from "@lib/openai.ts"; import tds from "https://cdn.skypack.dev/turndown@7.2.0"; -import { createLogger } from "@lib/log.ts"; +import { createLogger } from "@lib/log/index.ts"; import { createRecipe, Recipe } from "@lib/resource/recipes.ts"; import recipeSchema from "@lib/recipeSchema.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; @@ -136,8 +136,8 @@ async function processCreateRecipeFromUrl( const jsonLds = Array.from( document?.querySelectorAll( "script[type='application/ld+json']", - ) as HTMLScriptElement[], - ); + ), + ) as unknown as HTMLScriptElement[]; let recipe: z.infer | undefined = undefined; if (jsonLds.length > 0) { diff --git a/routes/api/tmdb/[id].ts b/routes/api/tmdb/[id].ts index d252ee3..4e62fac 100644 --- a/routes/api/tmdb/[id].ts +++ b/routes/api/tmdb/[id].ts @@ -9,7 +9,9 @@ type CachedMovieCredits = { }; const CACHE_INTERVAL = 1000 * 60 * 24 * 30; -const cache = createCache({ expires: CACHE_INTERVAL }); +const cache = createCache("movie-credits", { + expires: CACHE_INTERVAL, +}); const GET = async ( _req: Request, diff --git a/routes/api/tmdb/credits/[id].ts b/routes/api/tmdb/credits/[id].ts index 674ea25..f005e62 100644 --- a/routes/api/tmdb/credits/[id].ts +++ b/routes/api/tmdb/credits/[id].ts @@ -1,7 +1,7 @@ import { FreshContext } from "$fresh/server.ts"; import { getMovieCredits } from "@lib/tmdb.ts"; import { json } from "@lib/helpers.ts"; -import { createLogger } from "@lib/log.ts"; +import { createLogger } from "@lib/log/index.ts"; import { createCache } from "@lib/cache.ts"; type CachedMovieCredits = { @@ -10,7 +10,9 @@ type CachedMovieCredits = { }; const CACHE_INTERVAL = 1000 * 60 * 24 * 30; -const cache = createCache({ expires: CACHE_INTERVAL }); +const cache = createCache("movie-credits", { + expires: CACHE_INTERVAL, +}); const log = createLogger("api/tmdb"); diff --git a/static/global.css b/static/global.css index 49a3d87..78cc14a 100644 --- a/static/global.css +++ b/static/global.css @@ -112,3 +112,7 @@ input[type=number] { .items-52>* { height: 52px; } + +.highlight>pre { + text-wrap: wrap; +}