From ef58db01de326b72f07fc4d6ae96a2e742e8fa2b Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:43:40 +0100 Subject: [PATCH] feat: very barebones search --- dev/docker-compose.yml | 12 +- package.json | 2 + src/app/(public)/api/autocomplete/route.ts | 29 +++ src/app/(public)/api/posts/route.ts | 10 + src/app/(public)/page.tsx | 4 - .../app/PostBrowser/PostBrowser.tsx | 77 +++++--- src/components/app/Search/Search.tsx | 108 +++++++++++ src/instrumentation.ts | 43 ++-- src/lib/services/ephemeralStorage.ts | 12 ++ yarn.lock | 183 +++++++++++++++++- 10 files changed, 429 insertions(+), 51 deletions(-) create mode 100644 src/app/(public)/api/autocomplete/route.ts create mode 100644 src/components/app/Search/Search.tsx create mode 100644 src/lib/services/ephemeralStorage.ts diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index e79af75..130e396 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -8,4 +8,14 @@ services: volumes: - ./psql:/var/lib/postgresql/data ports: - - 5555:5432 \ No newline at end of file + - 5555:5432 + dragonfly: + image: 'docker.dragonflydb.io/dragonflydb/dragonfly' + ulimits: + memlock: -1 + ports: + - "6379:6379" + environment: + DRAGONFLY_PASSWORD: dfsjhkdswkjntelsmldbfvsgknl5t + volumes: + - ./draognfly:/data \ No newline at end of file diff --git a/package.json b/package.json index a8f75d8..1b86cb1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "cron": "^3.3.2", + "ioredis": "^5.4.2", "lucia": "^3.1.1", "lucide-react": "^0.368.0", "minio": "^8.0.3", @@ -37,6 +38,7 @@ "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", + "unstorage": "^1.14.4", "zod": "^3.24.1" }, "devDependencies": { diff --git a/src/app/(public)/api/autocomplete/route.ts b/src/app/(public)/api/autocomplete/route.ts new file mode 100644 index 0000000..4d53ef8 --- /dev/null +++ b/src/app/(public)/api/autocomplete/route.ts @@ -0,0 +1,29 @@ +import ephemeralStorage from '@/lib/services/ephemeralStorage'; +import type { NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get('q')!.toLowerCase().split(' ').at(-1); + if (query !== '' && !query) { + return new Response('Invalid query', { status: 400 }); + } + + const tags = await ephemeralStorage.keys('tag'); + const filteredTags = tags.filter((tag) => tag.replace('tag:', '').includes(query)); + + const mappedTags = await Promise.all( + filteredTags.map(async (t) => { + const getTag = parseInt((await ephemeralStorage.get(t)) as string); + return { tag: t.replace('tag:', ''), count: getTag }; + }) + ); + mappedTags.sort((a, b) => { + return b.count - a.count; + }); + + return new Response(JSON.stringify(mappedTags), { + headers: { + 'Content-Type': 'application/json', + }, + }); +} diff --git a/src/app/(public)/api/posts/route.ts b/src/app/(public)/api/posts/route.ts index 7855818..57d90c9 100644 --- a/src/app/(public)/api/posts/route.ts +++ b/src/app/(public)/api/posts/route.ts @@ -4,14 +4,24 @@ import { type NextRequest } from 'next/server'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const page = parseInt(searchParams.get('page')!); + const query = searchParams.get('q')?.trim().split(' ')!; if (page && isNaN(page)) { return new Response('Invalid page number', { status: 400 }); } + if (!query.length) { + return new Response('Invalid query', { status: 400 }); + } + // TODO: negative tags const queryPosts = await prisma.post.findMany({ take: 30, skip: page * 30, orderBy: { createdAt: 'desc' }, + where: { + tags: { + hasEvery: query[0] !== '' ? query : [], + }, + }, }); return new Response(JSON.stringify(queryPosts), { diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index f2a5dae..51b9815 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -1,10 +1,6 @@ import PostBrowser from '@/components/app/PostBrowser/PostBrowser'; -import prisma from '@/lib/db'; -import Image from 'next/image'; -import Link from 'next/link'; export default async function Home() { - const posts = await prisma.post.findMany({ take: 30, orderBy: { createdAt: 'desc' } }); return (

This is nextbooru

diff --git a/src/components/app/PostBrowser/PostBrowser.tsx b/src/components/app/PostBrowser/PostBrowser.tsx index f6105dd..aac847d 100644 --- a/src/components/app/PostBrowser/PostBrowser.tsx +++ b/src/components/app/PostBrowser/PostBrowser.tsx @@ -6,16 +6,24 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Post } from '@prisma/client'; import Image from 'next/image'; import Link from 'next/link'; +import Search from '../Search/Search'; export default function PostBrowser() { const [page, setPage] = React.useState(0); const [loading, setLoading] = React.useState(false); const [hasMore, setHasMore] = React.useState(true); const [posts, setPosts] = React.useState([]); + const [search, setSearch] = React.useState(''); - const next = async () => { + const next = async (reset = false, query = search) => { setLoading(true); - const res = await fetch(`/api/posts?page=${page}`); + const currentPage = reset ? 0 : page; + if (reset) { + setPosts([]); + setPage(0); + setHasMore(true); + } + const res = await fetch(`/api/posts?page=${currentPage}&q=${query}`); const newImages = await res.json(); if (!newImages.length) { setHasMore(false); @@ -28,37 +36,42 @@ export default function PostBrowser() { }; return ( - -
- {posts.map((post) => ( -
- - -
- ))} - {loading && - Array.from({ length: page === 0 ? 12 : 2 }).map((_, i) => ( -
- + <> + { + setSearch(q); + next(true, q); + }} /> + +
+ {posts.map((post) => ( +
+ +
))} -
+ {loading && + Array.from({ length: page === 0 ? 12 : 2 }).map((_, i) => ( +
+ +
+ ))} +
- {/* interactionobserver sentinel */} -
- + {/* interactionobserver sentinel */} +
+ + ); -} - +} \ No newline at end of file diff --git a/src/components/app/Search/Search.tsx b/src/components/app/Search/Search.tsx new file mode 100644 index 0000000..ad55915 --- /dev/null +++ b/src/components/app/Search/Search.tsx @@ -0,0 +1,108 @@ +'use client' + +import React, { useState, useEffect, useRef } from 'react' +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Search as SearchIcon } from 'lucide-react' + +export default function Search(props: Props) { + const [input, setInput] = useState('') + const [suggestions, setSuggestions] = useState<{ tag: string, count: string }[]>([]) + const [selectedIndex, setSelectedIndex] = useState(-1) + const inputRef = useRef(null) + + useEffect(() => { + const fetchSuggestions = async () => { + const lastTag = input.split(' ').pop() || '' + if (lastTag.length > 0) { + try { + const response = await fetch(`/api/autocomplete?q=${encodeURIComponent(lastTag)}`) + if (response.ok) { + const data = await response.json() + setSuggestions(data.slice(0, 5)) + } else { + setSuggestions([]) + } + } catch (error) { + setSuggestions([]) + } + } else { + setSuggestions([]) + } + setSelectedIndex(-1) + } + + fetchSuggestions() + }, [input]) + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + setSelectedIndex(prev => (prev < suggestions.length - 1 ? prev + 1 : prev)) + e.preventDefault() + } else if (e.key === 'ArrowUp') { + setSelectedIndex(prev => (prev > 0 ? prev - 1 : -1)) + e.preventDefault() + } else if (e.key === 'Enter' && selectedIndex >= 0) { + const tags = input.split(' ').slice(0, -1) + tags.push(suggestions[selectedIndex].tag) + setInput(tags.join(' ') + ' ') + setSuggestions([]) + e.preventDefault() + } else if (e.key === 'Escape') { + setSuggestions([]) + } else if (e.key === 'Enter') { + props.onSearch(input) + } + } + + const handleSuggestionClick = (suggestion: string) => { + const tags = input.split(' ').slice(0, -1) + tags.push(suggestion) + setInput(tags.join(' ') + ' ') + setSuggestions([]) + inputRef.current?.focus() + } + + return ( +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + className="pr-10 font-mono" + /> + +
+ {suggestions.length > 0 && ( +
    + {suggestions.map((suggestion, index) => ( +
  • handleSuggestionClick(suggestion.tag)} + > + {suggestion.tag} ({suggestion.count}) +
  • + ))} +
+ )} +
+ ) +} + +interface Props { + onSearch: (q: string) => void +} \ No newline at end of file diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 39114ef..cc88a19 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -1,16 +1,16 @@ import prisma from './lib/db'; +import ephemeralStorage from './lib/services/ephemeralStorage'; export async function register() { - if (process.env.SAFEBOORU_PULL !== 'true') return; if (process.env.NEXT_RUNTIME === 'nodejs') { - const { CronJob } = await import('cron'); const crypto = await import('crypto'); const { generateId } = await import('lucia'); - const fs = await import('fs/promises'); const { default: hashImage } = await import('@/lib/hashImage'); const minio = (await import('@/lib/services/minio')).default; - - const job = async () => { + const { performance } = await import('perf_hooks'); + + const safebooruJob = async () => { + if (process.env.SAFEBOORU_PULL !== 'true') return; console.log('Deleting prior safebooru posts and accounts...'); await prisma.post.deleteMany({ where: { author: { username: { startsWith: 'safebooru-' } } }, @@ -28,14 +28,13 @@ export async function register() { console.log('Pulling safebooru images...'); console.log('Fetching...'); const res = await fetch( - 'https://safebooru.org/index.php?page=dapi&s=post&q=index&json=1&limit=40', + 'https://safebooru.org/index.php?page=dapi&s=post&q=index&json=1&limit=300', { headers: { 'User-Agent': 'nextbooru' } } ); const posts = await res.json(); - - const genId = generateId(6); - + console.log('Creating account...'); + const genId = generateId(6); const account = await prisma.user.create({ data: { username: `safebooru-${genId}`, @@ -49,7 +48,6 @@ export async function register() { const savedFilename = `http${minioIsSSL ? 's' : ''}://${process.env.MINIO_ENDPOINT}/${ process.env.MINIO_BUCKET }/safebooru-${genId}-${post.id}.jpg`; - //await fs.writeFile(savedFilename, new Uint8Array(imageUrl)); await minio.putObject( process.env.MINIO_BUCKET!, `safebooru-${genId}-${post.id}.jpg`, @@ -68,9 +66,30 @@ export async function register() { console.log(`Downloaded id ${post.id}`); } }; + // await safebooruJob(); - await job(); + const writeTagsToEphemeral = async () => { + // TODO: move tags to another table. this is a temporary and inefficient solution. + const perfStart = performance.now(); + const posts = await prisma.post.findMany({ select: { tags: true } }); + const tags = posts.flatMap((post) => post.tags); - // new CronJob('0 */2 * * *', async () => await job(), null, true); + const occurrences: Record = {}; + for (const tag of tags) { + if (occurrences[tag]) { + occurrences[tag]++; + } else { + occurrences[tag] = 1; + } + } + + for (const [tag, count] of Object.entries(occurrences)) { + await ephemeralStorage.set(`tag:${tag}`, count); + } + const perfEnd = performance.now(); + + console.log(`Writing tags to ephemeral took ${Math.round(perfEnd - perfStart)}ms`); + } + await writeTagsToEphemeral(); } } diff --git a/src/lib/services/ephemeralStorage.ts b/src/lib/services/ephemeralStorage.ts new file mode 100644 index 0000000..bab0a6e --- /dev/null +++ b/src/lib/services/ephemeralStorage.ts @@ -0,0 +1,12 @@ +import { createStorage } from "unstorage"; +import redisDriver from 'unstorage/drivers/redis'; + +const ephemeralStorage = createStorage({ + driver: redisDriver({ + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + password: process.env.REDIS_PASSWORD, + }), +}); + +export default ephemeralStorage; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9505742..477486c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -473,6 +473,11 @@ resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1430,7 +1435,7 @@ any-promise@^1.0.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== -anymatch@~3.1.2: +anymatch@^3.1.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -1760,7 +1765,7 @@ chalk@^5.0.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== -chokidar@^3.5.3: +chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -1809,6 +1814,11 @@ clsx@^2.1.0, clsx@^2.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + code-block-writer@^12.0.0: version "12.0.0" resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770" @@ -1857,11 +1867,21 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +consola@^3.2.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.3.3.tgz#0dd8a2314b0f7bf18a49064138ad685f3346543d" + integrity sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg== + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-es@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.2.2.tgz#18ceef9eb513cac1cb6c14bcbf8bdb2679b34821" + integrity sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg== + cosmiconfig@^8.1.3: version "8.3.6" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" @@ -1898,6 +1918,13 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +"crossws@>=0.2.0 <0.4.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.3.1.tgz#7980e0b6688fe23286661c3ab8deeccbaa05ca86" + integrity sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw== + dependencies: + uncrypto "^0.1.3" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -2006,11 +2033,26 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +destr@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449" + integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ== + detect-libc@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" @@ -2792,6 +2834,22 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +h3@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.13.0.tgz#b5347a8936529794b6754b440e26c0ab8a60dceb" + integrity sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg== + dependencies: + cookie-es "^1.2.2" + crossws ">=0.2.0 <0.4.0" + defu "^6.1.4" + destr "^2.0.3" + iron-webcrypto "^1.2.1" + ohash "^1.1.4" + radix3 "^1.1.2" + ufo "^1.5.4" + uncrypto "^0.1.3" + unenv "^1.10.0" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -2903,11 +2961,31 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.2.tgz#ebb6f1a10b825b2c0fb114763d7e82114a0bee6c" + integrity sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ipaddr.js@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== +iron-webcrypto@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz#aa60ff2aa10550630f4c0b11fd2442becdb35a6f" + integrity sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg== + is-arguments@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" @@ -3327,6 +3405,16 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -3372,6 +3460,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.1.tgz#e8d901141f22937968e45a6533d52824070151e4" integrity sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA== +lru-cache@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -3452,6 +3545,11 @@ mime-types@^2.1.35: dependencies: mime-db "1.52.0" +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -3587,6 +3685,11 @@ node-domexception@^1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== +node-fetch-native@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" + integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== + node-fetch@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" @@ -3689,6 +3792,20 @@ object.values@^1.1.6, object.values@^1.1.7: define-properties "^1.2.1" es-object-atoms "^1.0.0" +ofetch@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.4.1.tgz#b6bf6b0d75ba616cef6519dd8b6385a8bae480ec" + integrity sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw== + dependencies: + destr "^2.0.3" + node-fetch-native "^1.6.4" + ufo "^1.5.4" + +ohash@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" + integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3819,6 +3936,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -3957,6 +4079,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +radix3@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.2.tgz#fd27d2af3896c6bf4bcdfab6427c69c2afc69ec0" + integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA== + react-dom@19: version "19.0.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" @@ -4041,6 +4168,18 @@ recast@^0.23.2: tiny-invariant "^1.3.3" tslib "^2.0.1" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" @@ -4333,6 +4472,11 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + stdin-discarder@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz#22b3e400393a8e28ebf53f9958f3880622efde21" @@ -4715,6 +4859,11 @@ typescript@^5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== +ufo@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -4725,16 +4874,46 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +uncrypto@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" + integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +unenv@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.10.0.tgz#c3394a6c6e4cfe68d699f87af456fe3f0db39571" + integrity sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ== + dependencies: + consola "^3.2.3" + defu "^6.1.4" + mime "^3.0.0" + node-fetch-native "^1.6.4" + pathe "^1.1.2" + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== +unstorage@^1.14.4: + version "1.14.4" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.14.4.tgz#620dd68997a3245fca1e04c0171335817525bc3d" + integrity sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg== + dependencies: + anymatch "^3.1.3" + chokidar "^3.6.0" + destr "^2.0.3" + h3 "^1.13.0" + lru-cache "^10.4.3" + node-fetch-native "^1.6.4" + ofetch "^1.4.1" + ufo "^1.5.4" + update-browserslist-db@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"