Ver código fonte

reduce overhead index requests (#342)

* reduce overhead requests

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>

* update index cache ttl

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>

* code clean

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>

* support clear cache

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>

* let => const

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

---------

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>
Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 1 ano atrás
pai
commit
ff492222cd

+ 2 - 0
client/src/http/BaseModel.ts

@@ -50,6 +50,8 @@ export default class BaseModel {
     } as any;
     if (timeout) httpConfig.timeout = timeout;
     const res = await http(httpConfig);
+    // conflict with collection view data structure, status is useless, so delete here.
+    delete res.data.data.status;
     return new this(res.data.data || {}) as T;
   }
 

+ 8 - 1
client/src/http/Collection.ts

@@ -42,13 +42,20 @@ export class Collection extends BaseModel implements CollectionData {
     return super.findAll({ path: this.COLLECTIONS_URL, params: data || {} });
   }
 
-  static getCollection(name: string) {
+  static getCollectionWithIndexInfo(name: string) {
     return super.search<Collection>({
       path: `${this.COLLECTIONS_URL}/${name}`,
       params: {},
     });
   }
 
+  static getCollectionInfo(collectionName: string) {
+    return super.search<Collection>({
+      path: `${this.COLLECTIONS_URL}/${collectionName}/info`,
+      params: {},
+    });
+  }
+
   static createCollection(data: any) {
     return super.create({ path: this.COLLECTIONS_URL, data });
   }

+ 6 - 0
client/src/http/MilvusIndex.ts

@@ -49,6 +49,12 @@ export class MilvusIndex extends BaseModel {
     return super.batchDelete({ path, data: { ...param, type } });
   }
 
+  static async flush() {
+    const path = `${this.BASE_URL}/flush`;
+
+    return super.query({ path, data: {} });
+  }
+
   get indexType() {
     return this.params.find(p => p.key === 'index_type')?.value || '';
   }

+ 6 - 1
client/src/pages/collections/Collections.tsx

@@ -9,7 +9,7 @@ import {
   dataContext,
   webSocketContext,
 } from '@/context';
-import { Collection, MilvusService, DataService } from '@/http';
+import { Collection, MilvusService, DataService, MilvusIndex } from '@/http';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
 import AttuGrid from '@/components/grid/Grid';
@@ -123,6 +123,10 @@ const Collections = () => {
     }
   }, [setCollections, checkCollectionStatus]);
 
+  const clearIndexCache = useCallback(async () => {
+    await MilvusIndex.flush();
+  }, []);
+
   useEffect(() => {
     fetchData();
   }, [fetchData, database]);
@@ -412,6 +416,7 @@ const Collections = () => {
     {
       type: 'iconBtn',
       onClick: () => {
+        clearIndexCache();
         fetchData();
       },
       label: collectionTrans('delete'),

+ 1 - 1
client/src/pages/partitions/Partitions.tsx

@@ -81,7 +81,7 @@ const Partitions: FC<{
   };
 
   const fetchCollectionDetail = async (name: string) => {
-    const res = await Collection.getCollection(name);
+    const res = await Collection.getCollectionInfo(name);
     return res;
   };
 

+ 1 - 1
client/src/pages/preview/Preview.tsx

@@ -45,7 +45,7 @@ const Preview: FC<{
 
   const loadData = async (collectionName: string) => {
     // get schema list
-    const collection = await Collection.getCollection(collectionName);
+    const collection = await Collection.getCollectionInfo(collectionName);
 
     const schemaList = collection.fields!;
     let nameList = schemaList.map(v => ({

+ 1 - 1
client/src/pages/query/Query.tsx

@@ -66,7 +66,7 @@ const Query: FC<{
   };
 
   const getFields = async (collectionName: string) => {
-    const collection = await Collection.getCollection(collectionName);
+    const collection = await Collection.getCollectionInfo(collectionName);
     const schemaList = collection.fields;
 
     const nameList = schemaList.map(v => ({

+ 3 - 1
client/src/pages/schema/Schema.tsx

@@ -85,7 +85,9 @@ const Schema: FC<{
       const KeyIcon = icons.key;
 
       try {
-        const collection = await Collection.getCollection(collectionName);
+        const collection = await Collection.getCollectionWithIndexInfo(
+          collectionName
+        );
         const fields = collection.fieldWithIndexInfo.map(f =>
           Object.assign(f, {
             _fieldNameElement: (

+ 9 - 10
client/src/pages/search/VectorSearch.tsx

@@ -15,7 +15,7 @@ import SimpleMenu from '@/components/menu/SimpleMenu';
 import { Option } from '@/components/customSelector/Types';
 import Filter from '@/components/advancedSearch';
 import { Field } from '@/components/advancedSearch/Types';
-import { Collection, MilvusIndex } from '@/http';
+import { Collection } from '@/http';
 import {
   parseValue,
   parseLocationSearch,
@@ -253,15 +253,17 @@ const VectorSearch = () => {
 
   const fetchFieldsWithIndex = useCallback(
     async (collectionName: string, collections: Collection[]) => {
-      const fields =
-        collections.find(c => c.collectionName === collectionName)?.fields ||
-        [];
-      const indexes = await MilvusIndex.getIndexInfo(collectionName);
+      const col = collections.find(c => c.collectionName === collectionName);
+
+      const fields = col?.fields ?? [];
 
       const { vectorFields, nonVectorFields } = classifyFields(fields);
 
       // only vector type fields can be select
-      const fieldOptions = getVectorFieldOptions(vectorFields, indexes);
+      const fieldOptions = getVectorFieldOptions(
+        vectorFields,
+        col?.indexes ?? []
+      );
       setFieldOptions(fieldOptions);
       if (fieldOptions.length > 0) {
         // set first option value as default field value
@@ -351,10 +353,7 @@ const VectorSearch = () => {
 
     setTableLoading(true);
     try {
-      const res = await Collection.vectorSearchData(
-        selectedCollection,
-        params
-      );
+      const res = await Collection.vectorSearchData(selectedCollection, params);
       setTableLoading(false);
       setSearchResult(res.results);
       setLatency(res.latency);

+ 2 - 1
client/src/pages/segments/Segments.tsx

@@ -28,7 +28,8 @@ const Segments: FC<{
     const qsegments = (await Segement.getQSegments(collectionName)) || {};
 
     const combinedArray = psegments.infos.map(p => {
-      const q = qsegments.infos.find(q => q.segmentID === p.segmentID)! as any;
+      const q: any =
+        qsegments.infos.find(q => q.segmentID === p.segmentID)! || {};
       return {
         ...p,
         ...Object.keys(q).reduce((acc, key) => {

+ 3 - 0
package.json

@@ -14,5 +14,8 @@
   "private": true,
   "dependencies": {
     "react-router-dom": "^6.20.0"
+  },
+  "devDependencies": {
+    "@types/lru-cache": "^7.10.10"
   }
 }

+ 2 - 2
server/package.json

@@ -26,7 +26,7 @@
     "glob": "^7.2.0",
     "helmet": "^7.0.0",
     "http-errors": "^2.0.0",
-    "lru-cache": "^6.0.0",
+    "lru-cache": "^10.1.0",
     "morgan": "^1.10.0",
     "node-cron": "^3.0.2",
     "rimraf": "^5.0.1",
@@ -55,7 +55,7 @@
     "@types/glob": "^8.1.0",
     "@types/http-errors": "^2.0.1",
     "@types/jest": "^29.5.3",
-    "@types/lru-cache": "^5.1.1",
+    "@types/lru-cache": "^7.10.10",
     "@types/morgan": "^1.9.4",
     "@types/node": "^20.4.2",
     "@types/node-cron": "^3.0.8",

+ 12 - 7
server/src/app.ts

@@ -3,7 +3,7 @@ import cors from 'cors';
 import helmet from 'helmet';
 import * as http from 'http';
 import { Server, Socket } from 'socket.io';
-import LruCache from 'lru-cache';
+import { LRUCache } from 'lru-cache';
 import * as path from 'path';
 import chalk from 'chalk';
 import { router as connectRouter } from './milvus';
@@ -21,15 +21,21 @@ import {
   ErrorMiddleware,
   ReqHeaderMiddleware,
 } from './middleware';
-import { EXPIRED_TIME, CACHE_KEY } from './utils';
+import { CLIENT_TTL, INDEX_TTL } from './utils';
 import { getIp } from './utils/Network';
+import { DescribeIndexResponse, MilvusClient } from './types';
 // initialize express app
 export const app = express();
 
 // initialize cache store
-const cache = new LruCache({
-  maxAge: EXPIRED_TIME,
-  updateAgeOnGet: true,
+export const clientCache = new LRUCache<string, MilvusClient>({
+  ttl: CLIENT_TTL,
+  ttlAutopurge: true,
+});
+
+export const indexCache = new LRUCache<string, DescribeIndexResponse>({
+  ttl: INDEX_TTL,
+  ttlAutopurge: true,
 });
 
 // initialize express router
@@ -52,9 +58,8 @@ router.get('/healthy', (req, res, next) => {
 const server = http.createServer(app);
 // default port 3000
 const PORT = 3000;
+
 // setup middlewares
-// use cache
-app.set(CACHE_KEY, cache);
 // use cors https://expressjs.com/en/resources/middleware/cors.html
 app.use(cors());
 // use helmet https://github.com/helmetjs/helmet

+ 15 - 0
server/src/collections/collections.controller.ts

@@ -50,7 +50,10 @@ export class CollectionController {
       this.renameCollection.bind(this)
     );
     this.router.delete('/:name/alias/:alias', this.dropAlias.bind(this));
+    // collection with index info
     this.router.get('/:name', this.describeCollection.bind(this));
+    // just collection info
+    this.router.get('/:name/info', this.getCollectionInfo.bind(this));
     this.router.get('/:name/count', this.count.bind(this));
 
     // load / release
@@ -164,6 +167,18 @@ export class CollectionController {
     }
   }
 
+  async getCollectionInfo(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    try {
+      const result = await this.collectionsService.describeCollection({
+        collection_name: name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
   async getCollectionStatistics(
     req: Request,
     res: Response,

+ 8 - 16
server/src/collections/collections.service.ts

@@ -4,7 +4,6 @@ import {
   DescribeCollectionReq,
   DropCollectionReq,
   GetCollectionStatisticsReq,
-  GetIndexStateReq,
   InsertReq,
   LoadCollectionReq,
   ReleaseLoadCollectionReq,
@@ -27,9 +26,14 @@ import { Parser } from '@json2csv/plainjs';
 import { throwErrorFromSDK, findKeyValue, genRows, ROW_COUNT } from '../utils';
 import { QueryDto, ImportSampleDto, GetReplicasDto } from './dto';
 import { CollectionData } from '../types';
+import { SchemaService } from '../schema/schema.service';
 
 export class CollectionsService {
-  constructor(private milvusService: MilvusService) {}
+  private schemaService: SchemaService;
+
+  constructor(private milvusService: MilvusService) {
+    this.schemaService = new SchemaService(milvusService);
+  }
 
   async getCollections(data?: ShowCollectionsReq) {
     const res = await this.milvusService.client.showCollections(data);
@@ -151,18 +155,6 @@ export class CollectionsService {
     return res;
   }
 
-  /**
-   * We do not throw error for this.
-   * Because if collection dont have index, it will throw error.
-   * We need wait for milvus error code.
-   * @param data
-   * @returns
-   */
-  async getIndexInfo(data: GetIndexStateReq) {
-    const res = await this.milvusService.client.describeIndex(data);
-    return res;
-  }
-
   /**
    * Get all collections meta data
    * @returns {id:string, collection_name:string, schema:Field[], autoID:boolean, rowCount: string, consistency_level:string}
@@ -190,7 +182,7 @@ export class CollectionsService {
         });
 
         // get index info for collections
-        const indexRes = await this.getIndexInfo({
+        const indexRes = await this.schemaService.describeIndex({
           collection_name: item.name,
         });
 
@@ -295,7 +287,7 @@ export class CollectionsService {
     const res = await this.getCollections();
     if (res.data.length > 0) {
       for (const item of res.data) {
-        const indexRes = await this.getIndexInfo({
+        const indexRes = await this.schemaService.describeIndex({
           collection_name: item.name,
         });
         data.push({

+ 4 - 4
server/src/middleware/index.ts

@@ -2,16 +2,16 @@ import { Request, Response, NextFunction } from 'express';
 import morgan from 'morgan';
 import chalk from 'chalk';
 import { MilvusService } from '../milvus/milvus.service';
-import { CACHE_KEY, MILVUS_ADDRESS, HTTP_STATUS_CODE } from '../utils';
+import { MILVUS_ADDRESS, HTTP_STATUS_CODE } from '../utils';
 import { HttpError } from 'http-errors';
 import HttpErrors from 'http-errors';
+import { clientCache } from '../app';
 
 export const ReqHeaderMiddleware = (
   req: Request,
   res: Response,
   next: NextFunction
 ) => {
-  const cache = req.app.get(CACHE_KEY);
   // all ape requests need set milvus address in header.
   // server will set active address in milvus service.
   const milvusAddress = (req.headers[MILVUS_ADDRESS] as string) || '';
@@ -20,10 +20,10 @@ export const ReqHeaderMiddleware = (
   //  only api request has MILVUS_ADDRESS.
   //  When client run in express, we dont need static files like: xx.js run this logic.
   //  Otherwise will cause 401 error.
-  if (milvusAddress && cache.has(milvusAddress)) {
+  if (milvusAddress && clientCache.has(milvusAddress)) {
     MilvusService.activeAddress = milvusAddress;
     // insight cache will update expire time when use insightCache.get
-    MilvusService.activeMilvusClient = cache.get(milvusAddress);
+    MilvusService.activeMilvusClient = clientCache.get(milvusAddress);
   }
 
   const CONNECT_URL = `/api/v1/milvus/connect`;

+ 7 - 8
server/src/milvus/milvus.controller.ts

@@ -2,7 +2,6 @@ import { NextFunction, Request, Response, Router } from 'express';
 import { dtoValidationMiddleware } from '../middleware/validation';
 import { MilvusService } from './milvus.service';
 import { ConnectMilvusDto, FlushDto, UseDatabaseDto } from './dto';
-import { CACHE_KEY } from '../utils';
 import packageJson from '../../package.json';
 
 export class MilvusController {
@@ -44,12 +43,13 @@ export class MilvusController {
 
   async connectMilvus(req: Request, res: Response, next: NextFunction) {
     const { address, username, password, database } = req.body;
-    const cache = req.app.get(CACHE_KEY);
     try {
-      const result = await this.milvusService.connectMilvus(
-        { address, username, password, database },
-        cache
-      );
+      const result = await this.milvusService.connectMilvus({
+        address,
+        username,
+        password,
+        database,
+      });
 
       res.send(result);
     } catch (error) {
@@ -60,10 +60,9 @@ export class MilvusController {
 
   async checkConnect(req: Request, res: Response, next: NextFunction) {
     const address = '' + req.query?.address;
-    const cache = req.app.get(CACHE_KEY);
 
     try {
-      const result = await this.milvusService.checkConnect(address, cache);
+      const result = await this.milvusService.checkConnect(address);
       res.send(result);
     } catch (error) {
       next(error);

+ 12 - 18
server/src/milvus/milvus.service.ts

@@ -4,11 +4,11 @@ import {
   GetMetricsResponse,
 } from '@zilliz/milvus2-sdk-node';
 import HttpErrors from 'http-errors';
-import LruCache from 'lru-cache';
 import { HTTP_STATUS_CODE } from '../utils/Const';
 import { DEFAULT_MILVUS_PORT } from '../utils';
 import { connectivityState } from '@grpc/grpc-js';
 import { DatabasesService } from '../database/databases.service';
+import { clientCache } from '../app';
 
 export class MilvusService {
   private databaseService: DatabasesService;
@@ -40,23 +40,17 @@ export class MilvusService {
         HTTP_STATUS_CODE.FORBIDDEN,
         'Can not find your connection, please check your connection settings.'
       );
-
-      // throw new Error('Please connect milvus first');
     }
   }
 
-  async connectMilvus(
-    data: {
-      address: string;
-      username?: string;
-      password?: string;
-      database?: string;
-    },
-    cache: LruCache<any, any>
-  ) {
+  async connectMilvus(data: {
+    address: string;
+    username?: string;
+    password?: string;
+    database?: string;
+  }) {
     // Destructure the data object to get the connection details
     const { address, username, password, database } = data;
-
     // Format the address to remove the http prefix
     const milvusAddress = MilvusService.formatAddress(address);
 
@@ -76,7 +70,7 @@ export class MilvusService {
         await milvusClient.connectPromise;
       } catch (error) {
         // If the connection fails, clear the cache and throw an error
-        cache.dump();
+        clientCache.dump();
         throw new Error('Failed to connect to Milvus: ' + error);
       }
 
@@ -90,7 +84,7 @@ export class MilvusService {
 
       // If the server is healthy, set the active address and add the client to the cache
       MilvusService.activeAddress = address;
-      cache.set(address, milvusClient);
+      clientCache.set(address, milvusClient);
 
       // Create a new database service and check if the specified database exists
       let hasDatabase = false;
@@ -109,14 +103,14 @@ export class MilvusService {
       return { address, database: hasDatabase ? database : 'default' };
     } catch (error) {
       // If any error occurs, clear the cache and throw the error
-      cache.dump();
+      clientCache.dump();
       throw error;
     }
   }
 
-  async checkConnect(address: string, cache: LruCache<any, any>) {
+  async checkConnect(address: string) {
     const milvusAddress = MilvusService.formatAddress(address);
-    return { connected: cache.has(milvusAddress) };
+    return { connected: clientCache.has(milvusAddress) };
   }
 
   async flush(data: FlushReq) {

+ 10 - 1
server/src/schema/schema.controller.ts

@@ -2,7 +2,6 @@ import { NextFunction, Request, Response, Router } from 'express';
 import { dtoValidationMiddleware } from '../middleware/validation';
 import { SchemaService } from './schema.service';
 import { milvusService } from '../milvus';
-
 import { ManageIndexDto } from './dto';
 
 export class SchemaController {
@@ -22,6 +21,7 @@ export class SchemaController {
     );
 
     this.router.get('/index', this.describeIndex.bind(this));
+    this.router.post('/index/flush', this.clearCache.bind(this));
 
     return this.router;
   }
@@ -60,4 +60,13 @@ export class SchemaController {
       next(error);
     }
   }
+
+  async clearCache(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.schemaService.clearCache();
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
 }

+ 49 - 6
server/src/schema/schema.service.ts

@@ -2,33 +2,76 @@ import {
   CreateIndexReq,
   DescribeIndexReq,
   DropIndexReq,
-  GetIndexBuildProgressReq,
-  GetIndexStateReq,
+  DescribeIndexResponse,
 } from '@zilliz/milvus2-sdk-node';
 import { throwErrorFromSDK } from '../utils/Error';
 import { MilvusService } from '../milvus/milvus.service';
+import { indexCache } from '../app';
 
 export class SchemaService {
   constructor(private milvusService: MilvusService) {}
 
   async createIndex(data: CreateIndexReq) {
     const res = await this.milvusService.client.createIndex(data);
+    const key = data.collection_name;
+
+    // clear cache;
+    indexCache.delete(key);
     throwErrorFromSDK(res);
     return res;
   }
 
+  /**
+   * This function is used to describe an index in Milvus.
+   * It first checks if the index description is cached, if so, it returns the cached value.
+   * If not, it calls the Milvus SDK's describeIndex function to get the index description.
+   * If the index is finished building, it caches the index description for future use.
+   * If the index is not finished building, it deletes any cached value for this index.
+   * @param data - The request data for describing an index. It contains the collection name.
+   * @returns - The response from the Milvus SDK's describeIndex function or the cached index description.
+   */
   async describeIndex(data: DescribeIndexReq) {
-    const res = await this.milvusService.client.describeIndex(data);
-    if (res.status.error_code === 'IndexNotExist') {
+    // Get the collection name from the request data
+    const key = data.collection_name;
+
+    // Try to get the index description from the cache
+    const value: DescribeIndexResponse = indexCache.get(key);
+
+    // If the index description is in the cache, return it
+    if (value) {
+      return value;
+    } else {
+      // If the index description is not in the cache, call the Milvus SDK's describeIndex function
+      const res = await this.milvusService.client.describeIndex(data);
+
+      // If the index is finished building and there is at least one index description,
+      // cache the index description for future use
+      if (
+        res.index_descriptions?.length > 0 &&
+        res.index_descriptions.every(i => i.state === 'Finished')
+      ) {
+        indexCache.set(key, res);
+      } else {
+        // If the index is not finished building, delete any cached value for this index
+        indexCache.delete(key);
+      }
+
+      // Return the response from the Milvus SDK's describeIndex function
       return res;
     }
-    throwErrorFromSDK(res.status);
-    return res;
   }
 
   async dropIndex(data: DropIndexReq) {
     const res = await this.milvusService.client.dropIndex(data);
+    const key = data.collection_name;
+
+    // clear cache;
+    indexCache.delete(key);
     throwErrorFromSDK(res);
     return res;
   }
+
+  async clearCache() {
+    return indexCache.clear();
+  }
 }

+ 2 - 0
server/src/types/index.ts

@@ -9,6 +9,8 @@ export {
   QuerySegmentInfo,
   GePersistentSegmentInfoResponse,
   PersistentSegmentInfo,
+  DescribeIndexResponse,
+  MilvusClient
 } from '@zilliz/milvus2-sdk-node';
 
 export * from './collections.type';

+ 4 - 2
server/src/utils/Const.ts

@@ -5,8 +5,10 @@ export const ROW_COUNT = 'row_count';
 export const MILVUS_ADDRESS = 'milvus-address';
 
 // for lru cache
-export const CACHE_KEY = 'insight_cache';
-export const EXPIRED_TIME = 1000 * 60 * 60 * 24;
+export const CLIENT_CACHE = 'insight_cache';
+export const INDEX_CACHE = 'index_cache';
+export const CLIENT_TTL = 1000 * 60 * 60 * 24;
+export const INDEX_TTL = 1000 * 60 * 60;
 
 export enum LOADING_STATE {
   LOADED,

+ 11 - 4
server/yarn.lock

@@ -1073,10 +1073,12 @@
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
   integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
 
-"@types/lru-cache@^5.1.1":
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef"
-  integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==
+"@types/lru-cache@^7.10.10":
+  version "7.10.10"
+  resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-7.10.10.tgz#3fa937c35ff4b3f6753d5737915c9bf8e693a713"
+  integrity sha512-nEpVRPWW9EBmx2SCfNn3ClYxPL7IktPX12HhIoSc/H5mMjdeW3+YsXIpseLQ2xF35+OcpwKQbEUw5VtqE4PDNA==
+  dependencies:
+    lru-cache "*"
 
 "@types/mime@^1":
   version "1.3.2"
@@ -3491,6 +3493,11 @@ lowercase-keys@^2.0.0:
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
   integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
 
+lru-cache@*, lru-cache@^10.1.0:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
+  integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
+
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"

+ 12 - 0
yarn.lock

@@ -7,6 +7,18 @@
   resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.0.tgz#7e29c4ee85176d9c08cb0f4456bff74d092c5065"
   integrity sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==
 
+"@types/lru-cache@^7.10.10":
+  version "7.10.10"
+  resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-7.10.10.tgz#3fa937c35ff4b3f6753d5737915c9bf8e693a713"
+  integrity sha512-nEpVRPWW9EBmx2SCfNn3ClYxPL7IktPX12HhIoSc/H5mMjdeW3+YsXIpseLQ2xF35+OcpwKQbEUw5VtqE4PDNA==
+  dependencies:
+    lru-cache "*"
+
+lru-cache@*:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
+  integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
+
 react-router-dom@^6.20.0:
   version "6.20.0"
   resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.20.0.tgz#7b9527a1e29c7fb90736a5f89d54ca01f40e264b"