From c9436f3284c34afa71e7146dd676d9de1d504f2f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 7 Oct 2023 17:08:41 -0400 Subject: [PATCH 01/24] refactor: use keyv for search caching with 1 min expirations --- api/server/routes/search.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/api/server/routes/search.js b/api/server/routes/search.js index 955e58da97a..6ece85f3483 100644 --- a/api/server/routes/search.js +++ b/api/server/routes/search.js @@ -1,3 +1,4 @@ +const Keyv = require('keyv'); const express = require('express'); const router = express.Router(); const { MeiliSearch } = require('meilisearch'); @@ -7,7 +8,8 @@ const { reduceHits } = require('../../lib/utils/reduceHits'); const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc'); const requireJwtAuth = require('../middleware/requireJwtAuth'); -const cache = new Map(); +const expiration = 60 * 1000; +const cache = new Keyv({ namespace: 'search', ttl: expiration }); router.get('/sync', async function (req, res) { await Message.syncWithMeili(); @@ -17,22 +19,18 @@ router.get('/sync', async function (req, res) { router.get('/', requireJwtAuth, async function (req, res) { try { - let user = req.user.id; - user = user ?? null; + let user = req.user.id ?? ''; const { q } = req.query; const pageNumber = req.query.pageNumber || 1; - const key = `${user || ''}${q}`; - - if (cache.has(key)) { + const key = `${user}${q}`; + const cached = await cache.get(key); + if (cached) { console.log('cache hit', key); - const cached = cache.get(key); const { pages, pageSize, messages } = cached; res .status(200) .send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages }); return; - } else { - cache.clear(); } // const message = await Message.meiliSearch(q); @@ -77,7 +75,7 @@ router.get('/', requireJwtAuth, async function (req, res) { result.messages = activeMessages; if (result.cache) { result.cache.messages = activeMessages; - cache.set(key, result.cache); + cache.set(key, result.cache, expiration); delete result.cache; } delete result.convoMap; From 8445876f9108e2cc0f5dd5bd270e66ba12f19a1f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 7 Oct 2023 20:40:26 -0400 Subject: [PATCH 02/24] feat: keyvRedis; chore: bump keyv, bun.lockb, add jsconfig for vscode file resolution --- api/cache/keyvRedis.js | 8 ++++ api/jsconfig.json | 13 ++++++ api/package.json | 3 +- bun.lockb | Bin 755985 -> 760395 bytes package-lock.json | 98 +++++++++++++++++++++++++++++++++++++++-- 5 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 api/cache/keyvRedis.js create mode 100644 api/jsconfig.json diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js new file mode 100644 index 00000000000..51e705fac2d --- /dev/null +++ b/api/cache/keyvRedis.js @@ -0,0 +1,8 @@ +const KeyvRedis = require('@keyv/redis'); +const { REDIS_URI } = process.env ?? {}; + +const keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); + +keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); + +module.exports = keyvRedis; diff --git a/api/jsconfig.json b/api/jsconfig.json new file mode 100644 index 00000000000..756746fbf81 --- /dev/null +++ b/api/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "CommonJS", + // "checkJs": true, // Report errors in JavaScript files + "baseUrl": "./", + "paths": { + "*": ["*", "node_modules/*"], + "~/*": ["./*"] + } + }, + "exclude": ["node_modules"] +} diff --git a/api/package.json b/api/package.json index f01b8c55c96..c76676ca994 100644 --- a/api/package.json +++ b/api/package.json @@ -24,6 +24,7 @@ "@anthropic-ai/sdk": "^0.5.4", "@azure/search-documents": "^11.3.2", "@keyv/mongo": "^2.1.8", + "@keyv/redis": "^2.8.0", "@waylaidwanderer/chatgpt-api": "^1.37.2", "axios": "^1.3.4", "bcryptjs": "^2.4.3", @@ -42,7 +43,7 @@ "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "keyv-file": "^0.2.0", "langchain": "^0.0.153", "lodash": "^4.17.21", diff --git a/bun.lockb b/bun.lockb index 1e71e31ce80da5d21de09df0b2b7b25015e9b9f6..7165010ab0dd01a50a7fbaff5f0ecb7eb73765e9 100755 GIT binary patch delta 51192 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 !^k=owdT-aG?cY(4lA7(i>sBe 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 diff --git a/package-lock.json b/package-lock.json index c4ca442c935..cafc9a06c68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@anthropic-ai/sdk": "^0.5.4", "@azure/search-documents": "^11.3.2", "@keyv/mongo": "^2.1.8", + "@keyv/redis": "^2.8.0", "@waylaidwanderer/chatgpt-api": "^1.37.2", "axios": "^1.3.4", "bcryptjs": "^2.4.3", @@ -63,7 +64,7 @@ "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "keyv-file": "^0.2.0", "langchain": "^0.0.153", "lodash": "^4.17.21", @@ -4850,6 +4851,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5419,6 +5425,17 @@ "@mongodb-js/saslprep": "^1.1.0" } }, + "node_modules/@keyv/redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-6k7wG/KKSIGpruKlsEB4sFjECJEyQsuJbWoWdoq9Uv2L6Mm/SEqEidekRZI/QljE1A4WQkFsIE8hHl1Oc3UNGg==", + "dependencies": { + "ioredis": "^5.3.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@librechat/backend": { "resolved": "api", "link": true @@ -9682,6 +9699,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -10399,6 +10424,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -13636,6 +13669,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "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" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -15222,9 +15278,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": { "json-buffer": "3.0.1" } @@ -15583,11 +15639,21 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -20348,6 +20414,25 @@ "node": ">=8" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -21397,6 +21482,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", From 3988a4ecd6025e6f839b34320e3d0c5ced1fe3c3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 7 Oct 2023 20:43:14 -0400 Subject: [PATCH 03/24] feat: api/search redis support --- api/server/routes/search.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/server/routes/search.js b/api/server/routes/search.js index 6ece85f3483..93ea3b25496 100644 --- a/api/server/routes/search.js +++ b/api/server/routes/search.js @@ -7,9 +7,14 @@ const { Conversation, getConvosQueried } = require('../../models/Conversation'); const { reduceHits } = require('../../lib/utils/reduceHits'); const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc'); const requireJwtAuth = require('../middleware/requireJwtAuth'); +const keyvRedis = require('../../cache/keyvRedis'); +const { isEnabled } = require('../utils'); const expiration = 60 * 1000; -const cache = new Keyv({ namespace: 'search', ttl: expiration }); +const cacheOptions = { namespace: 'search', ttl: expiration }; +const cache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv(cacheOptions); router.get('/sync', async function (req, res) { await Message.syncWithMeili(); @@ -22,7 +27,7 @@ router.get('/', requireJwtAuth, async function (req, res) { let user = req.user.id ?? ''; const { q } = req.query; const pageNumber = req.query.pageNumber || 1; - const key = `${user}${q}`; + const key = `${user}:search:${q}`; const cached = await cache.get(key); if (cached) { console.log('cache hit', key); From 9a0db35efefd51726ae55ad0a6cd7c67b60580c3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 8 Oct 2023 12:21:15 -0400 Subject: [PATCH 04/24] refactor(redis) use ioredis cluster for keyv fix(OpenID): when redis is configured, use redis memory store for express-session --- api/cache/keyvRedis.js | 4 +- api/cache/redis.js | 4 ++ api/package.json | 2 + api/server/socialLogins.js | 18 +++--- bun.lockb | Bin 760395 -> 760818 bytes package-lock.json | 110 +++++++++++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 api/cache/redis.js diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js index 51e705fac2d..04a46089491 100644 --- a/api/cache/keyvRedis.js +++ b/api/cache/keyvRedis.js @@ -1,7 +1,7 @@ const KeyvRedis = require('@keyv/redis'); -const { REDIS_URI } = process.env ?? {}; +const redis = require('./redis'); -const keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); +const keyvRedis = new KeyvRedis(redis, { useRedisSets: false }); keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); diff --git a/api/cache/redis.js b/api/cache/redis.js new file mode 100644 index 00000000000..adf291d02b6 --- /dev/null +++ b/api/cache/redis.js @@ -0,0 +1,4 @@ +const Redis = require('ioredis'); +const { REDIS_URI } = process.env ?? {}; +const redis = new Redis.Cluster(REDIS_URI); +module.exports = redis; diff --git a/api/package.json b/api/package.json index c76676ca994..4453c9c269c 100644 --- a/api/package.json +++ b/api/package.json @@ -30,6 +30,7 @@ "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", "cohere-ai": "^6.0.0", + "connect-redis": "^7.1.0", "cookie": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.0.3", @@ -40,6 +41,7 @@ "googleapis": "^118.0.0", "handlebars": "^4.7.7", "html": "^1.0.0", + "ioredis": "^5.3.2", "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", diff --git a/api/server/socialLogins.js b/api/server/socialLogins.js index ece87a2edc3..af61db73e9d 100644 --- a/api/server/socialLogins.js +++ b/api/server/socialLogins.js @@ -1,4 +1,5 @@ const session = require('express-session'); +const RedisStore = require('connect-redis').default; const passport = require('passport'); const { googleLogin, @@ -7,6 +8,7 @@ const { facebookLogin, setupOpenId, } = require('../strategies'); +const client = require('../cache/redis'); const configureSocialLogins = (app) => { if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { @@ -28,13 +30,15 @@ const configureSocialLogins = (app) => { process.env.OPENID_SCOPE && process.env.OPENID_SESSION_SECRET ) { - app.use( - session({ - secret: process.env.OPENID_SESSION_SECRET, - resave: false, - saveUninitialized: false, - }), - ); + const sessionOptions = { + secret: process.env.OPENID_SESSION_SECRET, + resave: false, + saveUninitialized: false, + }; + if (process.env.USE_REDIS) { + sessionOptions.store = new RedisStore({ client, prefix: 'librechat' }); + } + app.use(session(sessionOptions)); app.use(passport.session()); setupOpenId(); } diff --git a/bun.lockb b/bun.lockb index 7165010ab0dd01a50a7fbaff5f0ecb7eb73765e9..f1262564fe3033bc87361f5cab5612ee907d5775 100755 GIT binary patch delta 44851 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 B(?0+J diff --git a/package-lock.json b/package-lock.json index cafc9a06c68..b49d6b7b071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", "cohere-ai": "^6.0.0", + "connect-redis": "^7.1.0", "cookie": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.0.3", @@ -61,6 +62,7 @@ "googleapis": "^118.0.0", "handlebars": "^4.7.7", "html": "^1.0.0", + "ioredis": "^5.3.2", "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", @@ -6517,6 +6519,78 @@ "node": ">=8.x" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", + "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "optional": true, + "peer": true, + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true, + "peer": true + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", + "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", @@ -9881,6 +9955,17 @@ "node": ">=0.8" } }, + "node_modules/connect-redis": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.0.tgz", + "integrity": "sha512-UaqO1EirWjON2ENsyau7N5lbkrdYBpS6mYlXSeff/OYXsd6EGZ+SXSmNPoljL2PSua8fgjAEaldSA73PMZQ9Eg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "express-session": ">=1" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -12401,6 +12486,16 @@ "node": ">=12" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -20414,6 +20509,21 @@ "node": ">=8" } }, + "node_modules/redis": { + "version": "4.6.10", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", + "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "optional": true, + "peer": true, + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.11", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.6", + "@redis/search": "1.1.5", + "@redis/time-series": "1.0.5" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", From 74cc114636b6f76decc623c33fe6c7f9f1a73816 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 8 Oct 2023 17:45:05 -0400 Subject: [PATCH 05/24] fix: revert using uri for keyvredis --- api/cache/keyvRedis.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js index 04a46089491..51e705fac2d 100644 --- a/api/cache/keyvRedis.js +++ b/api/cache/keyvRedis.js @@ -1,7 +1,7 @@ const KeyvRedis = require('@keyv/redis'); -const redis = require('./redis'); +const { REDIS_URI } = process.env ?? {}; -const keyvRedis = new KeyvRedis(redis, { useRedisSets: false }); +const keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); From b2bd77d55d1308c7291e6226be09d4fc573352eb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 8 Oct 2023 20:06:28 -0400 Subject: [PATCH 06/24] fix(SearchBar): properly debounce search queries, fix weird render behaviors --- .../src/components/Messages/Content/Error.tsx | 1 - client/src/components/Messages/Message.tsx | 2 +- client/src/components/Nav/Nav.tsx | 11 +---- client/src/components/Nav/SearchBar.tsx | 42 +++++++++---------- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/client/src/components/Messages/Content/Error.tsx b/client/src/components/Messages/Content/Error.tsx index 5d19ef29562..ed6f4b280dc 100644 --- a/client/src/components/Messages/Content/Error.tsx +++ b/client/src/components/Messages/Content/Error.tsx @@ -7,7 +7,6 @@ const isJson = (str: string) => { try { JSON.parse(str); } catch (e) { - console.error(e); return false; } return true; diff --git a/client/src/components/Messages/Message.tsx b/client/src/components/Messages/Message.tsx index ef857f98a37..8f6f50e355a 100644 --- a/client/src/components/Messages/Message.tsx +++ b/client/src/components/Messages/Message.tsx @@ -170,7 +170,7 @@ export default function Message({ text={text ?? ''} message={message} enterEdit={enterEdit} - error={error ?? false} + error={!!(error && !searchResult)} isSubmitting={isSubmitting} unfinished={unfinished ?? false} isCreatedByUser={isCreatedByUser ?? true} diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index c51273d89b1..2e99d3ad270 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -13,7 +13,6 @@ import { Panel, Spinner } from '~/components'; import { Conversations, Pages } from '../Conversations'; import { useAuthContext, - useDebounce, useMediaQuery, useLocalize, useConversation, @@ -65,14 +64,8 @@ export default function Nav({ navVisible, setNavVisible }) { const [isFetching, setIsFetching] = useState(false); - const debouncedSearchTerm = useDebounce(searchQuery, 750); - const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber + '', { - enabled: !!( - !!debouncedSearchTerm && - debouncedSearchTerm.length > 0 && - isSearchEnabled && - isSearching - ), + const searchQueryFn = useSearchQuery(searchQuery, pageNumber + '', { + enabled: !!(!!searchQuery && searchQuery.length > 0 && isSearchEnabled && isSearching), }); const onSearchSuccess = useCallback((data: TSearchResults, expectedPage?: number) => { diff --git a/client/src/components/Nav/SearchBar.tsx b/client/src/components/Nav/SearchBar.tsx index 142daafa545..6f6fe6c21d5 100644 --- a/client/src/components/Nav/SearchBar.tsx +++ b/client/src/components/Nav/SearchBar.tsx @@ -1,6 +1,7 @@ -import { forwardRef, useState, useEffect, Ref } from 'react'; +import { forwardRef, useState, useCallback, useMemo, Ref } from 'react'; import { Search, X } from 'lucide-react'; -import { useRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; +import debounce from 'lodash/debounce'; import { useLocalize } from '~/hooks'; import store from '~/store'; @@ -10,33 +11,35 @@ type SearchBarProps = { const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) => { const { clearSearch } = props; - const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery); + const setSearchQuery = useSetRecoilState(store.searchQuery); const [showClearIcon, setShowClearIcon] = useState(false); + const [text, setText] = useState(''); const localize = useLocalize(); + const clearText = useCallback(() => { + setShowClearIcon(false); + setSearchQuery(''); + clearSearch(); + setText(''); + }, [setSearchQuery, clearSearch]); + const handleKeyUp = (e: React.KeyboardEvent) => { const { value } = e.target as HTMLInputElement; - /* TODO: deprecated keyCode */ - if (e.keyCode === 8 && value === '') { - setSearchQuery(''); - clearSearch(); + if (e.key === 'Backspace' && value === '') { + clearText(); } }; + const sendRequest = useCallback((value: string) => setSearchQuery(value), [setSearchQuery]); + const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]); + const onChange = (e: React.FormEvent) => { const { value } = e.target as HTMLInputElement; - setSearchQuery(value); setShowClearIcon(value.length > 0); + setText(value); + debouncedSendRequest(value); }; - useEffect(() => { - if (searchQuery.length === 0) { - setShowClearIcon(false); - } else { - setShowClearIcon(true); - } - }, [searchQuery]); - return (
) = { e.code === 'Space' ? e.stopPropagation() : null; @@ -58,10 +61,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = className={`absolute right-3 h-5 w-5 cursor-pointer ${ showClearIcon ? 'opacity-100' : 'opacity-0' } transition-opacity duration-1000`} - onClick={() => { - setSearchQuery(''); - clearSearch(); - }} + onClick={clearText} />
); From d0a237b155a885a4d0d21b85a86fc8483b193485 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 8 Oct 2023 20:32:17 -0400 Subject: [PATCH 07/24] refactor: add authentication to search endpoint and show error messages in results --- api/server/routes/search.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/server/routes/search.js b/api/server/routes/search.js index 93ea3b25496..8598b6639bd 100644 --- a/api/server/routes/search.js +++ b/api/server/routes/search.js @@ -16,13 +16,15 @@ const cache = isEnabled(process.env.USE_REDIS) ? new Keyv({ store: keyvRedis }) : new Keyv(cacheOptions); +router.use(requireJwtAuth); + router.get('/sync', async function (req, res) { await Message.syncWithMeili(); await Conversation.syncWithMeili(); res.send('synced'); }); -router.get('/', requireJwtAuth, async function (req, res) { +router.get('/', async function (req, res) { try { let user = req.user.id ?? ''; const { q } = req.query; @@ -70,7 +72,7 @@ router.get('/', requireJwtAuth, async function (req, res) { if (message.conversationId.includes('--')) { message.conversationId = cleanUpPrimaryKeyValue(message.conversationId); } - if (result.convoMap[message.conversationId] && !message.error) { + if (result.convoMap[message.conversationId]) { const convo = result.convoMap[message.conversationId]; const { title, chatGptLabel, model } = convo; message = { ...message, ...{ title, chatGptLabel, model } }; From 9b90b742742c4f2bbece4cf680e0d627a9f47639 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 9 Oct 2023 16:15:47 -0400 Subject: [PATCH 08/24] feat: redis support for violation logs --- api/cache/getLogStores.js | 34 ++++++++++++++++++++-------------- api/cache/logViolation.js | 10 ++++++---- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index 56839fcd2a6..446fe6fe65e 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -1,26 +1,33 @@ const Keyv = require('keyv'); const keyvMongo = require('./keyvMongo'); -const { math } = require('../server/utils'); +const keyvRedis = require('./keyvRedis'); +const { math, isEnabled } = require('../server/utils'); const { logFile, violationFile } = require('./keyvFiles'); const { BAN_DURATION } = process.env ?? {}; const duration = math(BAN_DURATION, 7200000); +const createViolationInstance = (namespace) => { + const config = isEnabled(process.env.USE_REDIS) + ? { store: keyvRedis } + : { store: violationFile, namespace }; + return new Keyv(config); +}; + const namespaces = { - ban: new Keyv({ store: keyvMongo, ttl: duration, namespace: 'bans' }), + ban: new Keyv({ store: keyvMongo, namespace: 'bans', duration }), general: new Keyv({ store: logFile, namespace: 'violations' }), - concurrent: new Keyv({ store: violationFile, namespace: 'concurrent' }), - non_browser: new Keyv({ store: violationFile, namespace: 'non_browser' }), - message_limit: new Keyv({ store: violationFile, namespace: 'message_limit' }), - token_balance: new Keyv({ store: violationFile, namespace: 'token_balance' }), - registrations: new Keyv({ store: violationFile, namespace: 'registrations' }), - logins: new Keyv({ store: violationFile, namespace: 'logins' }), + concurrent: createViolationInstance('concurrent'), + non_browser: createViolationInstance('non_browser'), + message_limit: createViolationInstance('message_limit'), + token_balance: createViolationInstance('token_balance'), + registrations: createViolationInstance('registrations'), + logins: createViolationInstance('logins'), }; /** - * Returns either the logs of violations specified by type if a type is provided - * or it returns the general log if no type is specified. If an invalid type is passed, - * an error will be thrown. + * Returns the keyv cache specified by type. + * If an invalid type is passed, an error will be thrown. * * @module getLogStores * @requires keyv - a simple key-value storage that allows you to easily switch out storage adapters. @@ -31,11 +38,10 @@ const namespaces = { * @throws Will throw an error if an invalid violation type is passed. */ const getLogStores = (type) => { - if (!type) { + if (!type || !namespaces[type]) { throw new Error(`Invalid store type: ${type}`); } - const logs = namespaces[type]; - return logs; + return namespaces[type]; }; module.exports = getLogStores; diff --git a/api/cache/logViolation.js b/api/cache/logViolation.js index 9f045421a92..1e928d521df 100644 --- a/api/cache/logViolation.js +++ b/api/cache/logViolation.js @@ -1,5 +1,6 @@ const getLogStores = require('./getLogStores'); const banViolation = require('./banViolation'); +const { isEnabled } = require('../server/utils'); /** * Logs the violation. @@ -17,10 +18,11 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => { } const logs = getLogStores('general'); const violationLogs = getLogStores(type); + const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId; - const userViolations = (await violationLogs.get(userId)) ?? 0; + const userViolations = (await violationLogs.get(key)) ?? 0; const violationCount = userViolations + score; - await violationLogs.set(userId, violationCount); + await violationLogs.set(key, violationCount); errorMessage.user_id = userId; errorMessage.prev_count = userViolations; @@ -28,10 +30,10 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => { errorMessage.date = new Date().toISOString(); await banViolation(req, res, errorMessage); - const userLogs = (await logs.get(userId)) ?? []; + const userLogs = (await logs.get(key)) ?? []; userLogs.push(errorMessage); delete errorMessage.user_id; - await logs.set(userId, userLogs); + await logs.set(key, userLogs); }; module.exports = logViolation; From c007ff457fde12142b70e22eab120a100d7a969f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 9 Oct 2023 19:47:41 -0400 Subject: [PATCH 09/24] fix(logViolation): ensure a number is always being stored in cache --- api/cache/logViolation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/cache/logViolation.js b/api/cache/logViolation.js index 1e928d521df..7fe85afd8a6 100644 --- a/api/cache/logViolation.js +++ b/api/cache/logViolation.js @@ -21,7 +21,7 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => { const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId; const userViolations = (await violationLogs.get(key)) ?? 0; - const violationCount = userViolations + score; + const violationCount = +userViolations + +score; await violationLogs.set(key, violationCount); errorMessage.user_id = userId; From 4984e41c710d34bc1c7fb365f7e2555fb0805fab Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Oct 2023 09:19:30 -0400 Subject: [PATCH 10/24] feat(concurrentLimiter): uses clearPendingReq, clears pendingReq on abort, redis support --- api/cache/clearPendingReq.js | 45 +++++++++++++++------ api/cache/getLogStores.js | 12 ++++-- api/cache/index.js | 3 +- api/server/middleware/abortMiddleware.js | 6 ++- api/server/middleware/concurrentLimiter.js | 46 ++++++++++------------ 5 files changed, 66 insertions(+), 46 deletions(-) diff --git a/api/cache/clearPendingReq.js b/api/cache/clearPendingReq.js index d31d51d78a2..068711d311b 100644 --- a/api/cache/clearPendingReq.js +++ b/api/cache/clearPendingReq.js @@ -1,29 +1,48 @@ -const Keyv = require('keyv'); -const { pendingReqFile } = require('./keyvFiles'); -const { LIMIT_CONCURRENT_MESSAGES } = process.env ?? {}; - -const keyv = new Keyv({ store: pendingReqFile, namespace: 'pendingRequests' }); +const getLogStores = require('./getLogStores'); +const { isEnabled } = require('../server/utils'); +const { USE_REDIS, LIMIT_CONCURRENT_MESSAGES } = process.env ?? {}; +const ttl = 1000 * 60 * 1; /** - * Clear pending requests from the cache. + * Clear or decrement pending requests from the cache. * Checks the environmental variable LIMIT_CONCURRENT_MESSAGES; - * if the rule is enabled ('true'), pending requests in the cache are cleared. + * if the rule is enabled ('true'), it either decrements the count of pending requests + * or deletes the key if the count is less than or equal to 1. * * @module clearPendingReq - * @requires keyv - * @requires keyvFiles + * @requires ./getLogStores + * @requires ../server/utils * @requires process * * @async * @function - * @returns {Promise} A promise that either clears 'pendingRequests' from store or resolves with no value. + * @param {Object} params - The parameters object. + * @param {string} params.userId - The user ID for which the pending requests are to be cleared or decremented. + * @param {Object} [params.cache] - An optional cache object to use. If not provided, a default cache will be fetched using getLogStores. + * @returns {Promise} A promise that either decrements the 'pendingRequests' count, deletes the key from the store, or resolves with no value. */ -const clearPendingReq = async () => { - if (LIMIT_CONCURRENT_MESSAGES?.toLowerCase() !== 'true') { +const clearPendingReq = async ({ userId, cache: _cache }) => { + if (!userId) { + return; + } else if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) { + return; + } + + const namespace = 'pending_req'; + const cache = _cache ?? getLogStores(namespace); + + if (!cache) { return; } - await keyv.clear(); + const key = `${USE_REDIS ? namespace : ''}:${userId ?? ''}`; + const currentReq = +((await cache.get(key)) ?? 0); + + if (currentReq && currentReq >= 1) { + await cache.set(key, currentReq - 1, ttl); + } else { + await cache.delete(key); + } }; module.exports = clearPendingReq; diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index 446fe6fe65e..2692b6933cc 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -3,18 +3,22 @@ const keyvMongo = require('./keyvMongo'); const keyvRedis = require('./keyvRedis'); const { math, isEnabled } = require('../server/utils'); const { logFile, violationFile } = require('./keyvFiles'); -const { BAN_DURATION } = process.env ?? {}; +const { BAN_DURATION, USE_REDIS } = process.env ?? {}; const duration = math(BAN_DURATION, 7200000); const createViolationInstance = (namespace) => { - const config = isEnabled(process.env.USE_REDIS) - ? { store: keyvRedis } - : { store: violationFile, namespace }; + const config = isEnabled(USE_REDIS) ? { store: keyvRedis } : { store: violationFile, namespace }; return new Keyv(config); }; +// Serve cache from memory so no need to clear it on startup/exit +const pending_req = isEnabled(USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'pending_req' }); + const namespaces = { + pending_req, ban: new Keyv({ store: keyvMongo, namespace: 'bans', duration }), general: new Keyv({ store: logFile, namespace: 'violations' }), concurrent: createViolationInstance('concurrent'), diff --git a/api/cache/index.js b/api/cache/index.js index 1edbf981d92..bb1e774183d 100644 --- a/api/cache/index.js +++ b/api/cache/index.js @@ -1,6 +1,5 @@ const keyvFiles = require('./keyvFiles'); const getLogStores = require('./getLogStores'); const logViolation = require('./logViolation'); -const clearPendingReq = require('./clearPendingReq'); -module.exports = { ...keyvFiles, getLogStores, logViolation, clearPendingReq }; +module.exports = { ...keyvFiles, getLogStores, logViolation }; diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index fc9a441556c..a65d09c884f 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -1,5 +1,6 @@ +const { sendMessage, sendError, countTokens, isEnabled } = require('../utils'); const { saveMessage, getConvo, getConvoTitle } = require('../../models'); -const { sendMessage, sendError, countTokens } = require('../utils'); +const clearPendingReq = require('../../cache/clearPendingReq'); const spendTokens = require('../../models/spendTokens'); const abortControllers = require('./abortControllers'); @@ -20,6 +21,9 @@ async function abortMessage(req, res) { const handleAbort = () => { return async (req, res) => { try { + if (isEnabled(process.env.LIMIT_CONCURRENT_MESSAGES)) { + await clearPendingReq({ userId: req.user.id }); + } return await abortMessage(req, res); } catch (err) { console.error(err); diff --git a/api/server/middleware/concurrentLimiter.js b/api/server/middleware/concurrentLimiter.js index d110b1b86f8..402152eb029 100644 --- a/api/server/middleware/concurrentLimiter.js +++ b/api/server/middleware/concurrentLimiter.js @@ -1,10 +1,13 @@ -const Keyv = require('keyv'); -const { logViolation } = require('../../cache'); - +const clearPendingReq = require('../../cache/clearPendingReq'); +const { logViolation, getLogStores } = require('../../cache'); const denyRequest = require('./denyRequest'); -// Serve cache from memory so no need to clear it on startup/exit -const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); +const { + USE_REDIS, + CONCURRENT_MESSAGE_MAX = 1, + CONCURRENT_VIOLATION_SCORE: score, +} = process.env ?? {}; +const ttl = 1000 * 60 * 1; /** * Middleware to limit concurrent requests for a user. @@ -12,7 +15,7 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); * This middleware checks if a user has exceeded a specified concurrent request limit. * If the user exceeds the limit, an error is returned. If the user is within the limit, * their request count is incremented. After the request is processed, the count is decremented. - * If the `pendingReqCache` store is not available, the middleware will skip its logic. + * If the `cache` store is not available, the middleware will skip its logic. * * @function * @param {Object} req - Express request object containing user information. @@ -21,7 +24,9 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); * @throws {Error} Throws an error if the user exceeds the concurrent request limit. */ const concurrentLimiter = async (req, res, next) => { - if (!pendingReqCache) { + const namespace = 'pending_req'; + const cache = getLogStores(namespace); + if (!cache) { return next(); } @@ -29,12 +34,12 @@ const concurrentLimiter = async (req, res, next) => { return next(); } - const { CONCURRENT_MESSAGE_MAX = 1, CONCURRENT_VIOLATION_SCORE: score } = process.env; + const userId = req.user?.id ?? req.user?._id ?? ''; const limit = Math.max(CONCURRENT_MESSAGE_MAX, 1); const type = 'concurrent'; - const userId = req.user?.id ?? req.user?._id ?? null; - const pendingRequests = (await pendingReqCache.get(userId)) ?? 0; + const key = `${USE_REDIS ? namespace : ''}:${userId}`; + const pendingRequests = +((await cache.get(key)) ?? 0); if (pendingRequests >= limit) { const errorMessage = { @@ -46,22 +51,17 @@ const concurrentLimiter = async (req, res, next) => { await logViolation(req, res, type, errorMessage, score); return await denyRequest(req, res, errorMessage); } else { - await pendingReqCache.set(userId, pendingRequests + 1); + await cache.set(key, pendingRequests + 1, ttl); } // Ensure the requests are removed from the store once the request is done + let cleared = false; const cleanUp = async () => { - if (!pendingReqCache) { + if (cleared) { return; } - - const currentRequests = await pendingReqCache.get(userId); - - if (currentRequests && currentRequests >= 1) { - await pendingReqCache.set(userId, currentRequests - 1); - } else { - await pendingReqCache.delete(userId); - } + cleared = true; + await clearPendingReq({ userId, cache }); }; if (pendingRequests < limit) { @@ -72,10 +72,4 @@ const concurrentLimiter = async (req, res, next) => { next(); }; -// if cache is not served from memory, clear it on exit -// process.on('exit', async () => { -// console.log('Clearing all pending requests before exiting...'); -// await pendingReqCache.clear(); -// }); - module.exports = concurrentLimiter; From 60a77a6732295271eca72318cc9e3b7d139df495 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Oct 2023 09:37:47 -0400 Subject: [PATCH 11/24] fix(api/search/enable): query only when authenticated --- client/src/routes/Root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/routes/Root.tsx b/client/src/routes/Root.tsx index 4da530b2d40..c4711fb02e9 100644 --- a/client/src/routes/Root.tsx +++ b/client/src/routes/Root.tsx @@ -29,8 +29,8 @@ export default function Root() { const setEndpointsConfig = useSetRecoilState(store.endpointsConfig); const setModelsConfig = useSetRecoilState(store.modelsConfig); - const searchEnabledQuery = useGetSearchEnabledQuery(); const endpointsQuery = useGetEndpointsQuery(); + const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated }); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated }); const presetsQuery = useGetPresetsQuery({ enabled: !!user }); From 6c3ad98f7c86779175193f19607d08076c97d885 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Oct 2023 15:27:10 -0400 Subject: [PATCH 12/24] feat(ModelService): redis support --- api/server/services/ModelService.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/server/services/ModelService.js b/api/server/services/ModelService.js index 41d4290ffb4..7789e70cf2f 100644 --- a/api/server/services/ModelService.js +++ b/api/server/services/ModelService.js @@ -1,9 +1,13 @@ const Keyv = require('keyv'); const axios = require('axios'); +const { isEnabled } = require('../utils'); +const keyvRedis = require('../../cache/keyvRedis'); // const { getAzureCredentials, genAzureChatCompletion } = require('../../utils/'); const { openAIApiKey, userProvidedOpenAI } = require('./EndpointService').config; -const modelsCache = new Keyv({ namespace: 'models' }); +const modelsCache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'models' }); const { OPENROUTER_API_KEY, OPENAI_REVERSE_PROXY, CHATGPT_MODELS, ANTHROPIC_MODELS } = process.env ?? {}; From 2c53646d045ac674ebb6c39109843203371226fe Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Oct 2023 15:28:05 -0400 Subject: [PATCH 13/24] feat(checkBan): redis support --- api/server/middleware/checkBan.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/api/server/middleware/checkBan.js b/api/server/middleware/checkBan.js index 294f4a668bf..c744dda07d5 100644 --- a/api/server/middleware/checkBan.js +++ b/api/server/middleware/checkBan.js @@ -3,8 +3,11 @@ const uap = require('ua-parser-js'); const { getLogStores } = require('../../cache'); const denyRequest = require('./denyRequest'); const { isEnabled, removePorts } = require('../utils'); +const keyvRedis = require('../../cache/keyvRedis'); -const banCache = new Keyv({ namespace: 'bans', ttl: 0 }); +const banCache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'bans', ttl: 0 }); const message = 'Your account has been temporarily banned due to violations of our service.'; /** @@ -50,9 +53,11 @@ const checkBan = async (req, res, next = () => {}) => { req.ip = removePorts(req); const userId = req.user?.id ?? req.user?._id ?? null; + const ipKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:ip:${req.ip}` : req.ip; + const userKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:user:${userId}` : userId; - const cachedIPBan = await banCache.get(req.ip); - const cachedUserBan = await banCache.get(userId); + const cachedIPBan = await banCache.get(ipKey); + const cachedUserBan = await banCache.get(userKey); const cachedBan = cachedIPBan || cachedUserBan; if (cachedBan) { @@ -78,13 +83,13 @@ const checkBan = async (req, res, next = () => {}) => { const timeLeft = Number(isBanned.expiresAt) - Date.now(); if (timeLeft <= 0) { - await banLogs.delete(req.ip); - await banLogs.delete(userId); + await banLogs.delete(ipKey); + await banLogs.delete(userKey); return next(); } - banCache.set(req.ip, isBanned, timeLeft); - banCache.set(userId, isBanned, timeLeft); + banCache.set(ipKey, isBanned, timeLeft); + banCache.set(userKey, isBanned, timeLeft); req.banned = true; return await banResponse(req, res); }; From 94185830f8dd35cc2fbbe89967a46af6a86bbc77 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Oct 2023 15:28:40 -0400 Subject: [PATCH 14/24] refactor(api/search): consolidate keyv logic --- api/server/routes/search.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/server/routes/search.js b/api/server/routes/search.js index 8598b6639bd..98720a2ae5c 100644 --- a/api/server/routes/search.js +++ b/api/server/routes/search.js @@ -11,10 +11,9 @@ const keyvRedis = require('../../cache/keyvRedis'); const { isEnabled } = require('../utils'); const expiration = 60 * 1000; -const cacheOptions = { namespace: 'search', ttl: expiration }; const cache = isEnabled(process.env.USE_REDIS) ? new Keyv({ store: keyvRedis }) - : new Keyv(cacheOptions); + : new Keyv({ namespace: 'search', ttl: expiration }); router.use(requireJwtAuth); From f1bdb98b14cdfa69e3c3a35b67f34cf452f25222 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Oct 2023 21:18:45 -0400 Subject: [PATCH 15/24] fix(ci): add default empty value for REDIS_URI --- api/cache/keyvRedis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js index 51e705fac2d..b77dc4a43c8 100644 --- a/api/cache/keyvRedis.js +++ b/api/cache/keyvRedis.js @@ -1,5 +1,5 @@ const KeyvRedis = require('@keyv/redis'); -const { REDIS_URI } = process.env ?? {}; +const { REDIS_URI = '' } = process.env ?? {}; const keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); From 18809724aa3ec5021587f631c84d22ac861312b5 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Oct 2023 21:23:11 -0400 Subject: [PATCH 16/24] refactor(keyvRedis): use condition to initialize keyvRedis assignment --- api/cache/keyvRedis.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js index b77dc4a43c8..942b1b239fa 100644 --- a/api/cache/keyvRedis.js +++ b/api/cache/keyvRedis.js @@ -1,8 +1,14 @@ const KeyvRedis = require('@keyv/redis'); -const { REDIS_URI = '' } = process.env ?? {}; -const keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); +const { REDIS_URI } = process.env; -keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); +let keyvRedis; + +if (REDIS_URI) { + keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); + keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); +} else { + // console.log('REDIS_URI not provided. Redis module will not be initialized.'); +} module.exports = keyvRedis; From 68fcbf6001daa0f13ba00a2c7236423032223d27 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 14:06:08 -0400 Subject: [PATCH 17/24] refactor(connectDb): handle disconnected state (should create a new conn) --- api/lib/db/connectDb.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/lib/db/connectDb.js b/api/lib/db/connectDb.js index 8b9cdae012b..3e711ca7ad4 100644 --- a/api/lib/db/connectDb.js +++ b/api/lib/db/connectDb.js @@ -18,11 +18,12 @@ if (!cached) { } async function connectDb() { - if (cached.conn) { + if (cached.conn && cached.conn?._readyState === 1) { return cached.conn; } - if (!cached.promise) { + const disconnected = cached.conn && cached.conn?._readyState !== 1; + if (!cached.promise || disconnected) { const opts = { useNewUrlParser: true, useUnifiedTopology: true, From e43549b96a04ec185ac2cdc1b7a22b17d9c53c05 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 14:07:00 -0400 Subject: [PATCH 18/24] fix(ci/e2e): handle case where cleanUp did not successfully run --- e2e/setup/authenticate.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/e2e/setup/authenticate.ts b/e2e/setup/authenticate.ts index 9e91efd9f2c..6abac7a7e41 100644 --- a/e2e/setup/authenticate.ts +++ b/e2e/setup/authenticate.ts @@ -1,8 +1,10 @@ import { Page, FullConfig, chromium } from '@playwright/test'; +import cleanupUser from './cleanupUser'; import dotenv from 'dotenv'; dotenv.config(); type User = { email: string; name: string; password: string }; +const timeout = 3500; async function register(page: Page, user: User) { await page.getByRole('link', { name: 'Sign up' }).click(); @@ -52,18 +54,31 @@ async function authenticate(config: FullConfig, user: User) { }); console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState); - await page.goto(baseURL, { timeout: 5000 }); + await page.goto(baseURL, { timeout }); await register(page, user); - await page.waitForURL(`${baseURL}/chat/new`); + try { + await page.waitForURL(`${baseURL}/chat/new`, { timeout }); + } catch (error) { + console.error('Error:', error); + const userExists = page.getByTestId('registration-error'); + if (userExists) { + console.log('🤖: 🚨 user already exists'); + await cleanupUser(user); + await page.goto(baseURL, { timeout }); + await register(page, user); + } else { + throw new Error('🤖: 🚨 user failed to register'); + } + } console.log('🤖: ✔️ user successfully registered'); // Logout await logout(page, user); - await page.waitForURL(`${baseURL}/login`); + await page.waitForURL(`${baseURL}/login`, { timeout }); console.log('🤖: ✔️ user successfully logged out'); await login(page, user); - await page.waitForURL(`${baseURL}/chat/new`); + await page.waitForURL(`${baseURL}/chat/new`, { timeout }); console.log('🤖: ✔️ user successfully authenticated'); await page.context().storageState({ path: storageState as string }); From a067a97bf8c3617a6433ee25840d91ebdd37e3e1 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 16:11:31 -0400 Subject: [PATCH 19/24] fix(getDefaultEndpoint): return endpoint from localStorage if defined and endpointsConfig is default --- client/src/store/endpoints.ts | 21 ++++++++++++--------- client/src/utils/getDefaultEndpoint.ts | 15 +++++++++++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/client/src/store/endpoints.ts b/client/src/store/endpoints.ts index cbcd3e54fcb..458c40ad319 100644 --- a/client/src/store/endpoints.ts +++ b/client/src/store/endpoints.ts @@ -1,17 +1,19 @@ import { atom, selector } from 'recoil'; import { TEndpointsConfig } from 'librechat-data-provider'; +const defaultConfig: TEndpointsConfig = { + azureOpenAI: null, + openAI: null, + bingAI: null, + chatGPTBrowser: null, + gptPlugins: null, + google: null, + anthropic: null, +}; + const endpointsConfig = atom({ key: 'endpointsConfig', - default: { - azureOpenAI: null, - openAI: null, - bingAI: null, - chatGPTBrowser: null, - gptPlugins: null, - google: null, - anthropic: null, - }, + default: defaultConfig, }); const plugins = selector({ @@ -58,4 +60,5 @@ export default { endpointsConfig, endpointsFilter, availableEndpoints, + defaultConfig, }; diff --git a/client/src/utils/getDefaultEndpoint.ts b/client/src/utils/getDefaultEndpoint.ts index 1b960996e53..4f1e9f74b63 100644 --- a/client/src/utils/getDefaultEndpoint.ts +++ b/client/src/utils/getDefaultEndpoint.ts @@ -28,11 +28,18 @@ const getEndpointFromSetup = (convoSetup: TConvoSetup, endpointsConfig: TEndpoin const getEndpointFromLocalStorage = (endpointsConfig: TEndpointsConfig) => { try { const { lastConversationSetup } = getLocalStorageItems(); + const { endpoint } = lastConversationSetup; + const isDefaultConfig = Object.values(endpointsConfig ?? {})?.every((value) => !value); - return ( - lastConversationSetup.endpoint && - (endpointsConfig[lastConversationSetup.endpoint] ? lastConversationSetup.endpoint : null) - ); + if (isDefaultConfig && endpoint) { + return endpoint; + } + + if (isDefaultConfig && endpoint) { + return endpoint; + } + + return endpoint && endpointsConfig[endpoint] ? endpoint : null; } catch (error) { console.error(error); return null; From 533a9d43b8272121f6be68859547e90f9b5a81ac Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 16:23:22 -0400 Subject: [PATCH 20/24] ci(e2e): remove afterAll messages as startup/cleanUp will clear messages --- e2e/specs/messages.spec.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/e2e/specs/messages.spec.ts b/e2e/specs/messages.spec.ts index 76cf0a5409e..f9593c06c2c 100644 --- a/e2e/specs/messages.spec.ts +++ b/e2e/specs/messages.spec.ts @@ -38,14 +38,6 @@ test.beforeAll(async ({ browser }) => { await page.close(); }); -test.afterAll(async () => { - console.log('🤖: clearing conversations after message tests.'); - const page = await beforeAfterAllContext.newPage(); - await clearConvos(page); - await page.close(); - await beforeAfterAllContext.close(); -}); - test.beforeEach(async ({ page }) => { await page.goto(initialUrl, { timeout: 5000 }); }); From dd3f27cb002bbc3c0a3af1ece05f9ae264149340 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 16:33:39 -0400 Subject: [PATCH 21/24] ci(e2e): remove teardown for CI until further notice --- e2e/playwright.config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index ea45f1665fc..74a8e44d5ec 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -6,7 +6,11 @@ dotenv.config(); export default defineConfig({ globalSetup: require.resolve('./setup/global-setup'), - globalTeardown: require.resolve('./setup/global-teardown'), + // Github CI will hang on globalTeardown for unknown reasons: + // https://github.com/microsoft/playwright/issues/24071 + // https://github.com/microsoft/playwright/issues/24159 + // https://github.com/microsoft/playwright/issues/27048 + // globalTeardown: require.resolve('./setup/global-teardown'), testDir: 'specs/', outputDir: 'specs/.test-results', /* Run tests in files in parallel. From 9b1e901423431fc4fec4d262d2996185ba3453d4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 16:45:30 -0400 Subject: [PATCH 22/24] chore: bump playwright/test --- package-lock.json | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index b49d6b7b071..a676c063e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "packages/*" ], "devDependencies": { - "@playwright/test": "^1.32.1", + "@playwright/test": "^1.38.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-env": "^7.0.3", @@ -5550,12 +5550,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", - "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", "dev": true, "dependencies": { - "playwright": "1.38.0" + "playwright": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -18583,12 +18583,12 @@ } }, "node_modules/playwright": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", - "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", "devOptional": true, "dependencies": { - "playwright-core": "1.38.0" + "playwright-core": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -18601,9 +18601,9 @@ } }, "node_modules/playwright-core": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", - "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", "devOptional": true, "bin": { "playwright-core": "cli.js" diff --git a/package.json b/package.json index 3a9578c8ebb..eb6401d37bf 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ }, "homepage": "https://github.com/danny-avila/LibreChat#readme", "devDependencies": { - "@playwright/test": "^1.32.1", + "@playwright/test": "^1.38.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-env": "^7.0.3", From f88ddf948a84699c0d6f2b6cd9cafba1802a2cf4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 16:46:26 -0400 Subject: [PATCH 23/24] ci(e2e): reinstate teardown as CI issue is specific to github env --- e2e/playwright.config.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 74a8e44d5ec..ea45f1665fc 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -6,11 +6,7 @@ dotenv.config(); export default defineConfig({ globalSetup: require.resolve('./setup/global-setup'), - // Github CI will hang on globalTeardown for unknown reasons: - // https://github.com/microsoft/playwright/issues/24071 - // https://github.com/microsoft/playwright/issues/24159 - // https://github.com/microsoft/playwright/issues/27048 - // globalTeardown: require.resolve('./setup/global-teardown'), + globalTeardown: require.resolve('./setup/global-teardown'), testDir: 'specs/', outputDir: 'specs/.test-results', /* Run tests in files in parallel. From 34e611d424737c96de50f324a8fcce2d0d23bf64 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Oct 2023 17:02:12 -0400 Subject: [PATCH 24/24] fix(ci): click settings menu trigger by testid --- client/src/components/Nav/NavLinks.tsx | 1 + e2e/specs/nav.spec.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/components/Nav/NavLinks.tsx b/client/src/components/Nav/NavLinks.tsx index e04b6ed27cf..8ca9febb022 100644 --- a/client/src/components/Nav/NavLinks.tsx +++ b/client/src/components/Nav/NavLinks.tsx @@ -55,6 +55,7 @@ export default function NavLinks() { 'group-ui-open:bg-gray-800 flex w-full items-center gap-2.5 rounded-md px-3 py-3 text-sm transition-colors duration-200 hover:bg-gray-800', open ? 'bg-gray-800' : '', )} + data-testid="nav-user" >
diff --git a/e2e/specs/nav.spec.ts b/e2e/specs/nav.spec.ts index d8f99705829..ff1f5e16b39 100644 --- a/e2e/specs/nav.spec.ts +++ b/e2e/specs/nav.spec.ts @@ -4,14 +4,14 @@ test.describe('Navigation suite', () => { test('Navigation bar', async ({ page }) => { await page.goto('http://localhost:3080/', { timeout: 5000 }); - await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click(); - const navBar = await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').isVisible(); - expect(navBar).toBeTruthy(); + await page.getByTestId('nav-user').click(); + const navSettings = await page.getByTestId('nav-user').isVisible(); + expect(navSettings).toBeTruthy(); }); test('Settings modal', async ({ page }) => { await page.goto('http://localhost:3080/', { timeout: 5000 }); - await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click(); + await page.getByTestId('nav-user').click(); await page.getByText('Settings').click(); const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible();