diff --git a/examples/base-handler/src/app/api/cache-widget/[[...segments]]/route.ts b/examples/base-handler/src/app/api/cache-widget/[[...segments]]/route.ts index 2ec5437..223d337 100644 --- a/examples/base-handler/src/app/api/cache-widget/[[...segments]]/route.ts +++ b/examples/base-handler/src/app/api/cache-widget/[[...segments]]/route.ts @@ -1,11 +1,33 @@ -import { getCacheData } from "@nimpl/cache-tools"; +import { createRouteHelpers } from "@nimpl/cache-tools"; // eslint-disable-next-line @typescript-eslint/no-require-imports const cacheHandler = require("../../../../../cache-handler.js"); -export const GET = async (_request: Request, { params }: { params: Promise<{ segments?: string[] }> }) => { +const { getCacheData, putCacheData, deleteCacheData } = createRouteHelpers(cacheHandler); + +type RouteParams = { params: Promise<{ segments?: string[] }> }; + +export const GET = async (_request: Request, { params }: RouteParams) => { + const { segments } = await params; + const data = await getCacheData(segments); + + if (!data) return new Response("", { status: 404 }); + + return new Response(JSON.stringify(data)); +}; + +export const PUT = async (_request: Request, { params }: RouteParams) => { + const { segments } = await params; + const data = await putCacheData(segments); + + if (!data) return new Response("", { status: 404 }); + + return new Response(JSON.stringify(data)); +}; + +export const DELETE = async (_request: Request, { params }: RouteParams) => { const { segments } = await params; - const data = await getCacheData(cacheHandler, segments); + const data = await deleteCacheData(segments); if (!data) return new Response("", { status: 404 }); diff --git a/package.json b/package.json index dc33134..ed7b9a0 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ }, "resolutions": { "@nimpl/cache-adapter>@nimpl/cache": "workspace:*", - "@nimpl/cache-server>@nimpl/cache": "workspace:*" + "@nimpl/cache-server>@nimpl/cache": "workspace:*", + "@nimpl/cache-tools>@nimpl/cache": "workspace:*" }, "license": "MIT", "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" diff --git a/packages/cache-tools/package.json b/packages/cache-tools/package.json index 77d1c84..648d213 100644 --- a/packages/cache-tools/package.json +++ b/packages/cache-tools/package.json @@ -40,6 +40,7 @@ "url": "https://github.com/alexdln/" }, "devDependencies": { + "@nimpl/cache": "latest", "@rollup/plugin-commonjs": "29.0.0", "@rollup/plugin-node-resolve": "16.0.3", "@rollup/plugin-terser": "0.4.4", diff --git a/packages/cache-tools/src/create-cache.ts b/packages/cache-tools/src/create-cache.ts index 18cde0b..a427ade 100644 --- a/packages/cache-tools/src/create-cache.ts +++ b/packages/cache-tools/src/create-cache.ts @@ -1,4 +1,5 @@ -import { type CacheHandler, type Metadata } from "./lib/types"; +import { type CacheHandler, type Metadata } from "@nimpl/cache"; + import { objectToStream, streamToRaw } from "./lib/stream"; export const cache = diff --git a/packages/cache-tools/src/create-helpers.ts b/packages/cache-tools/src/create-helpers.ts index 334ed60..d55ea30 100644 --- a/packages/cache-tools/src/create-helpers.ts +++ b/packages/cache-tools/src/create-helpers.ts @@ -1,103 +1,17 @@ -import { type CacheHandler, type KeysData } from "./lib/types"; -import { streamToRaw } from "./lib/stream"; +import { type CacheHandler } from "@nimpl/cache"; -export const getKeys = async ( - cacheHandler: CacheHandler, - type: "main" | "persistent" | "ephemeral" = "main", -): Promise => { - const layers = { - main: cacheHandler, - persistent: cacheHandler.persistentLayer, - ephemeral: cacheHandler.ephemeralLayer, - }; - const handler = layers[type]; - return handler.keys(); -}; - -export const getKeyDetails = async ( - cacheHandler: CacheHandler, - type: "main" | "persistent" | "ephemeral", - key: string, -) => { - const layers = { - main: cacheHandler, - persistent: cacheHandler.persistentLayer, - ephemeral: cacheHandler.ephemeralLayer, - }; - const handler = layers[type]; - try { - const cacheEntry = await handler.getEntry(key); - - if (!cacheEntry) { - return { - key, - metadata: null, - value: null, - size: 0, - status: null, - }; - } - - const { entry, size, status } = cacheEntry; - const [cacheStream, responseStream] = entry.value.tee(); - entry.value = cacheStream; - const value = await streamToRaw(responseStream); - - return { - key, - metadata: { - tags: entry.tags, - timestamp: entry.timestamp, - stale: entry.stale, - revalidate: entry.revalidate, - expire: entry.expire, - }, - value, - size, - status, - }; - } catch (error) { - return { - key, - metadata: null, - value: null, - size: 0, - error: error instanceof Error ? error.message : "Unknown error", - status: null, - }; - } -}; - -export const getCacheData = (cacheHandler: CacheHandler, segments?: string[]) => { - if (!segments?.length || segments.length > 2) { - return null; - } - const type = segments[0] as "main" | "persistent" | "ephemeral"; - if (!["main", "persistent", "ephemeral"].includes(type)) { - return null; - } - if (segments.length === 1) { - return getKeys(cacheHandler, type); - } - - return getKeyDetails(cacheHandler, type, segments[1]); -}; - -export const updateTags = async (cacheHandler: CacheHandler, tags: string[], duration: number) => { - return cacheHandler.updateTags(tags, { expire: duration }); -}; - -export const updateKey = async (cacheHandler: CacheHandler, key: string, duration: number) => { - return cacheHandler.updateKey(key, { expire: duration }); -}; +import { type LayerType } from "./lib/types"; +import { getKeys, getKeyDetails } from "./lib/get-helpers"; +import { updateKey, updateTags } from "./lib/put-helpers"; +import { deleteKey } from "./lib/delete-helpers"; export const createHelpers = (cacheHandler: CacheHandler) => { return { - getKeys: (type: "main" | "persistent" | "ephemeral") => getKeys(cacheHandler, type), - getKeyDetails: (type: "main" | "persistent" | "ephemeral", key: string) => - getKeyDetails(cacheHandler, type, key), - getCacheData: (segments?: string[]) => getCacheData(cacheHandler, segments), - updateTags: (tags: string[], duration: number) => updateTags(cacheHandler, tags, duration), - updateKey: (key: string, duration: number) => updateKey(cacheHandler, key, duration), + getKeys: (type: LayerType) => getKeys(cacheHandler, type), + getKeyDetails: (type: LayerType, key: string) => getKeyDetails(cacheHandler, type, key), + updateTags: (type: LayerType, tags: string[], duration: number) => + updateTags(cacheHandler, type, tags, duration), + updateKey: (type: LayerType, key: string, duration: number) => updateKey(cacheHandler, type, key, duration), + deleteKey: (type: LayerType, key: string) => deleteKey(cacheHandler, type, key), }; }; diff --git a/packages/cache-tools/src/create-route.ts b/packages/cache-tools/src/create-route.ts new file mode 100644 index 0000000..8f69d06 --- /dev/null +++ b/packages/cache-tools/src/create-route.ts @@ -0,0 +1,13 @@ +import { type CacheHandler } from "@nimpl/cache"; + +import { getCacheData } from "./lib/get-helpers"; +import { putCacheData } from "./lib/put-helpers"; +import { deleteCacheData } from "./lib/delete-helpers"; + +export const createRouteHelpers = (cacheHandler: CacheHandler) => { + return { + getCacheData: (segments?: string[]) => getCacheData(cacheHandler, segments), + putCacheData: (segments?: string[]) => putCacheData(cacheHandler, segments), + deleteCacheData: (segments?: string[]) => deleteCacheData(cacheHandler, segments), + }; +}; diff --git a/packages/cache-tools/src/index.ts b/packages/cache-tools/src/index.ts index 459555d..f3ad22a 100644 --- a/packages/cache-tools/src/index.ts +++ b/packages/cache-tools/src/index.ts @@ -1,3 +1,3 @@ export * from "./create-cache"; export * from "./create-helpers"; -export * from "./lib/types"; +export * from "./create-route"; diff --git a/packages/cache-tools/src/lib/constants.ts b/packages/cache-tools/src/lib/constants.ts new file mode 100644 index 0000000..611e249 --- /dev/null +++ b/packages/cache-tools/src/lib/constants.ts @@ -0,0 +1 @@ +export const LAYER_TYPES = ["main", "persistent", "ephemeral"] as const; diff --git a/packages/cache-tools/src/lib/delete-helpers.ts b/packages/cache-tools/src/lib/delete-helpers.ts new file mode 100644 index 0000000..70127ba --- /dev/null +++ b/packages/cache-tools/src/lib/delete-helpers.ts @@ -0,0 +1,28 @@ +import { CacheHandler } from "@nimpl/cache"; + +import { type LayerType } from "./types"; +import { LAYER_TYPES } from "./constants"; + +export const deleteKey = async (cacheHandler: CacheHandler, type: LayerType, key: string) => { + const layers = { + main: cacheHandler, + persistent: cacheHandler.persistentLayer, + ephemeral: cacheHandler.ephemeralLayer, + }; + return layers[type].delete(key); +}; + +export const deleteCacheData = async (cacheHandler: CacheHandler, segments?: string[]) => { + if (!segments?.length || segments.length > 3) { + return null; + } + const type = segments[0] as LayerType; + if (!LAYER_TYPES.includes(type)) { + return null; + } + if (segments[1] === "key") { + await deleteKey(cacheHandler, type, segments[2]); + return true; + } + return null; +}; diff --git a/packages/cache-tools/src/lib/get-helpers.ts b/packages/cache-tools/src/lib/get-helpers.ts new file mode 100644 index 0000000..200438c --- /dev/null +++ b/packages/cache-tools/src/lib/get-helpers.ts @@ -0,0 +1,80 @@ +import { type CacheHandler } from "@nimpl/cache"; + +import { type LayerType } from "./types"; +import { streamToRaw } from "./stream"; +import { LAYER_TYPES } from "./constants"; + +export const getKeys = async (cacheHandler: CacheHandler, type: LayerType = "main"): Promise => { + const layers = { + main: cacheHandler, + persistent: cacheHandler.persistentLayer, + ephemeral: cacheHandler.ephemeralLayer, + }; + const handler = layers[type]; + return handler.keys(); +}; + +export const getKeyDetails = async (cacheHandler: CacheHandler, type: LayerType, key: string) => { + const layers = { + main: cacheHandler, + persistent: cacheHandler.persistentLayer, + ephemeral: cacheHandler.ephemeralLayer, + }; + const handler = layers[type]; + try { + const cacheEntry = await handler.getEntry(key); + + if (!cacheEntry) { + return { + key, + metadata: null, + value: null, + size: 0, + status: null, + }; + } + + const { entry, size, status } = cacheEntry; + const [cacheStream, responseStream] = entry.value.tee(); + entry.value = cacheStream; + const value = await streamToRaw(responseStream); + + return { + key, + metadata: { + tags: entry.tags, + timestamp: entry.timestamp, + stale: entry.stale, + revalidate: entry.revalidate, + expire: entry.expire, + }, + value, + size, + status, + }; + } catch (error) { + return { + key, + metadata: null, + value: null, + size: 0, + error: error instanceof Error ? error.message : "Unknown error", + status: null, + }; + } +}; + +export const getCacheData = (cacheHandler: CacheHandler, segments?: string[]) => { + if (!segments?.length || segments.length > 2) { + return null; + } + const type = segments[0] as LayerType; + if (!LAYER_TYPES.includes(type)) { + return null; + } + if (segments.length === 1) { + return getKeys(cacheHandler, type); + } + + return getKeyDetails(cacheHandler, type, segments[1]); +}; diff --git a/packages/cache-tools/src/lib/put-helpers.ts b/packages/cache-tools/src/lib/put-helpers.ts new file mode 100644 index 0000000..e5a8972 --- /dev/null +++ b/packages/cache-tools/src/lib/put-helpers.ts @@ -0,0 +1,41 @@ +import { CacheHandler } from "@nimpl/cache"; + +import { type LayerType } from "./types"; +import { LAYER_TYPES } from "./constants"; + +export const updateTags = async (cacheHandler: CacheHandler, type: LayerType, tags: string[], duration?: number) => { + const layers = { + main: cacheHandler, + persistent: cacheHandler.persistentLayer, + ephemeral: cacheHandler.ephemeralLayer, + }; + return layers[type].updateTags(tags, duration ? { expire: duration } : undefined); +}; + +export const updateKey = async (cacheHandler: CacheHandler, type: LayerType, key: string, duration?: number) => { + const layers = { + main: cacheHandler, + persistent: cacheHandler.persistentLayer, + ephemeral: cacheHandler.ephemeralLayer, + }; + return layers[type].updateKey(key, duration ? { expire: duration } : undefined); +}; + +export const putCacheData = async (cacheHandler: CacheHandler, segments?: string[]) => { + if (!segments?.length || segments.length > 3) { + return null; + } + const type = segments[0] as LayerType; + if (!LAYER_TYPES.includes(type)) { + return null; + } + if (segments[1] === "key") { + await updateKey(cacheHandler, type, segments[2]); + return true; + } + if (segments[1] === "tag") { + await updateTags(cacheHandler, type, [segments[2]]); + return true; + } + return null; +}; diff --git a/packages/cache-tools/src/lib/types.ts b/packages/cache-tools/src/lib/types.ts index 24e7ddc..0113260 100644 --- a/packages/cache-tools/src/lib/types.ts +++ b/packages/cache-tools/src/lib/types.ts @@ -1,40 +1,3 @@ -import { type ReadableStream as WebReadableStream } from "node:stream/web"; +import { type LAYER_TYPES } from "./constants"; -export type Metadata = { - tags: string[]; - timestamp: number; - stale: number; - expire: number; - revalidate: number; -}; - -export type KeysData = string[]; - -type Entry = { - value: ReadableStream | WebReadableStream; -} & Metadata; - -type CacheEntry = { - entry: Entry; - size: number; - status: string; -}; - -export type CacheHandler = { - getEntry: (key: string) => Promise; - get: (key: string) => Promise; - set: (key: string, value: Promise) => Promise; - keys: () => Promise; - updateTags: (tags: string[], durations?: { expire?: number }) => Promise; - updateKey: (key: string, durations?: { expire?: number }) => Promise; - ephemeralLayer: { - getEntry: (key: string) => Promise; - get: (key: string) => Promise; - keys: () => Promise; - }; - persistentLayer: { - getEntry: (key: string) => Promise; - get: (key: string) => Promise; - keys: () => Promise; - }; -}; +export type LayerType = (typeof LAYER_TYPES)[number]; diff --git a/packages/cache-widget/src/components/details/details.scss b/packages/cache-widget/src/components/details/details.scss index 78917d1..b5ea3ef 100644 --- a/packages/cache-widget/src/components/details/details.scss +++ b/packages/cache-widget/src/components/details/details.scss @@ -89,3 +89,7 @@ padding: 8px 16px; color: #852a00; } + +.__ncw_details-entry-actions { + padding: 8px 16px; +} diff --git a/packages/cache-widget/src/components/details/index.tsx b/packages/cache-widget/src/components/details/index.tsx index e9935c7..0297633 100644 --- a/packages/cache-widget/src/components/details/index.tsx +++ b/packages/cache-widget/src/components/details/index.tsx @@ -10,6 +10,7 @@ import { ErrorMessage } from "../error"; import { Reload } from "../reload"; import "./details.scss"; +import { EntryActions } from "../entry-actions"; export const Details: React.FC = () => { const { data, loading, error, reload } = use(FetchDetailsContext); @@ -103,6 +104,7 @@ export const Details: React.FC = () => { )} {data?.value && } + {data && } ); diff --git a/packages/cache-widget/src/components/entry-actions/entry-actions.scss b/packages/cache-widget/src/components/entry-actions/entry-actions.scss new file mode 100644 index 0000000..99b915a --- /dev/null +++ b/packages/cache-widget/src/components/entry-actions/entry-actions.scss @@ -0,0 +1,22 @@ +.__ncw_entry-actions { + display: flex; + gap: 10px; +} + +.__ncw_entry-actions-button { + padding: 8px 16px; + border: none; + border-radius: 6px; + background-color: #414141; + color: #ffffff; + cursor: pointer; + + &:hover { + background-color: #212121; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/packages/cache-widget/src/components/entry-actions/index.tsx b/packages/cache-widget/src/components/entry-actions/index.tsx new file mode 100644 index 0000000..bdcdebf --- /dev/null +++ b/packages/cache-widget/src/components/entry-actions/index.tsx @@ -0,0 +1,69 @@ +import React, { use, useCallback } from "react"; +import { + ApiUrlContext, + CacheKeyContext, + CategoryContext, + FetchDetailsContext, + FetchKeysContext, + SetCacheKeyContext, +} from "../../store/contexts"; +import { useFetch } from "../../lib/use-fetch"; + +import "./entry-actions.scss"; + +interface EntryActionsProps { + className?: string; +} + +export const EntryActions: React.FC = ({ className = "" }) => { + const apiUrl = use(ApiUrlContext); + const cacheKey = use(CacheKeyContext); + const category = use(CategoryContext); + const setCacheKey = use(SetCacheKeyContext); + const { reload: reloadDetails, loading } = use(FetchDetailsContext); + const { reload: reloadKeys } = use(FetchKeysContext); + const { fetch: revalidateFetch, loading: revalidateLoading } = useFetch(); + const { fetch: deleteFetch, loading: deleteLoading } = useFetch(); + + const revalidateHandler = useCallback(async () => { + await revalidateFetch( + apiUrl.endsWith("/") ? `${apiUrl}${category}/key/${cacheKey}/` : `${apiUrl}/${category}/key/${cacheKey}`, + { + method: "PUT", + }, + ); + await reloadDetails(); + }, [cacheKey, apiUrl, category, reloadDetails]); + + const deleteHandler = useCallback(async () => { + await deleteFetch( + apiUrl.endsWith("/") ? `${apiUrl}${category}/key/${cacheKey}/` : `${apiUrl}/${category}/key/${cacheKey}`, + { + method: "DELETE", + }, + ); + await reloadKeys(); + setCacheKey(null); + }, [cacheKey, apiUrl, category, reloadKeys, setCacheKey]); + + return ( +
+ + +
+ ); +}; diff --git a/packages/cache-widget/src/lib/use-fetch.ts b/packages/cache-widget/src/lib/use-fetch.ts index faf2dda..1d73464 100644 --- a/packages/cache-widget/src/lib/use-fetch.ts +++ b/packages/cache-widget/src/lib/use-fetch.ts @@ -4,7 +4,7 @@ export interface UseFetchReturn { data: T | null | undefined; loading: boolean; error: string | null; - fetch: (apiUrl: string) => Promise; + fetch: (apiUrl: string, options?: RequestInit) => Promise; reload: () => Promise; reset: () => void; } @@ -25,7 +25,7 @@ export function useFetch(defaultUrl?: string): UseFetchReturn { }, []); const fetchData = useCallback( - async (apiUrl: string) => { + async (apiUrl: string, options?: RequestInit) => { abortCurrentRequest(); const controller = new AbortController(); abortControllerRef.current = controller; @@ -35,7 +35,7 @@ export function useFetch(defaultUrl?: string): UseFetchReturn { setError(null); try { - const response = await fetch(apiUrl, { signal: controller.signal }); + const response = await fetch(apiUrl, { signal: controller.signal, ...options }); if (controller.signal.aborted) { return; diff --git a/packages/cache-widget/src/store/contexts.ts b/packages/cache-widget/src/store/contexts.ts index 8a21072..49842e7 100644 --- a/packages/cache-widget/src/store/contexts.ts +++ b/packages/cache-widget/src/store/contexts.ts @@ -3,6 +3,8 @@ import { createContext } from "react"; import { type CacheKeyInfo, type KeysData, type Category } from "../lib/types"; import { useFetch } from "../lib/use-fetch"; +export const ApiUrlContext = createContext("/api/cache-widget"); + export const WidgetOpenContext = createContext(false); export const SetWidgetOpenContext = createContext<(open: boolean) => void>(() => {}); diff --git a/packages/cache-widget/src/store/provider.tsx b/packages/cache-widget/src/store/provider.tsx index 5544144..acf73b0 100644 --- a/packages/cache-widget/src/store/provider.tsx +++ b/packages/cache-widget/src/store/provider.tsx @@ -4,6 +4,7 @@ import React, { useState, useCallback } from "react"; import { type Category, type CacheKeyInfo, type KeysData } from "../lib/types"; import { + ApiUrlContext, WidgetOpenContext, SetWidgetOpenContext, CacheKeyContext, @@ -70,22 +71,24 @@ export const CacheWidgetProvider: React.FC = ({ apiUrl ); return ( - - - - - - - - - {children} - - - - - - - - + + + + + + + + + + {children} + + + + + + + + + ); }; diff --git a/packages/cache/src/lib/helpers.ts b/packages/cache/src/lib/helpers.ts index 8ad45df..a7a7761 100644 --- a/packages/cache/src/lib/helpers.ts +++ b/packages/cache/src/lib/helpers.ts @@ -21,7 +21,8 @@ export const getUpdatedMetadata = ( durations: Durations | undefined, now: number, ): Metadata => { - if (!metadata.tags.some((tag) => tags.includes(tag))) return metadata; + const isEmptyTags = metadata.tags.length === 0 && tags.length === 0; + if (!isEmptyTags && !metadata.tags.some((tag) => tags.includes(tag))) return metadata; return { ...metadata, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59a99d9..c828186 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: overrides: '@nimpl/cache-adapter>@nimpl/cache': workspace:* '@nimpl/cache-server>@nimpl/cache': workspace:* + '@nimpl/cache-tools>@nimpl/cache': workspace:* importers: @@ -226,6 +227,9 @@ importers: packages/cache-tools: devDependencies: + '@nimpl/cache': + specifier: workspace:* + version: link:../cache '@rollup/plugin-commonjs': specifier: 29.0.0 version: 29.0.0(rollup@4.53.3)