diff --git a/src/backend/src/clients/redis/deleteRedisKeys.ts b/src/backend/src/clients/redis/deleteRedisKeys.ts new file mode 100644 index 000000000..fc2033434 --- /dev/null +++ b/src/backend/src/clients/redis/deleteRedisKeys.ts @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { redisClient } from './redisSingleton.js'; + +export const deleteRedisKeys = async (...keysInput: string[]) => { + const keys = keysInput + .map(key => key === null || key === undefined ? '' : String(key)) + .filter(Boolean); + + if ( keys.length === 0 ) { + return 0; + } + + let deleted = 0; + for ( const key of new Set(keys) ) { + deleted += await redisClient.del(key); + } + return deleted; +}; diff --git a/src/backend/src/modules/apps/AppInformationService.js b/src/backend/src/modules/apps/AppInformationService.js index bd756242a..9bb706788 100644 --- a/src/backend/src/modules/apps/AppInformationService.js +++ b/src/backend/src/modules/apps/AppInformationService.js @@ -20,6 +20,7 @@ const { origin_from_url } = require('../../util/urlutil'); const { DB_READ } = require('../../services/database/consts'); const BaseService = require('../../services/BaseService'); const { redisClient } = require('../../clients/redis/redisSingleton'); +const { deleteRedisKeys } = require('../../clients/redis/deleteRedisKeys.js'); const { AppRedisCacheSpace } = require('./AppRedisCacheSpace.js'); // Currently leaks memory (not sure why yet, but icons are a factor) @@ -215,97 +216,97 @@ class AppInformationService extends BaseService { const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); switch ( period ) { - case 'today': - return { - start: today.getTime(), - end: now.getTime(), - }; - case 'yesterday': { - const yesterday = new Date(today); + case 'today': + return { + start: today.getTime(), + end: now.getTime(), + }; + case 'yesterday': { + const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); return { start: yesterday.getTime(), end: today.getTime() - 1, }; - } - case '7d': { - const weekAgo = new Date(now); + } + case '7d': { + const weekAgo = new Date(now); weekAgo.setDate(weekAgo.getDate() - 7); return { start: weekAgo.getTime(), end: now.getTime(), }; - } - case '30d': { - const monthAgo = new Date(now); + } + case '30d': { + const monthAgo = new Date(now); monthAgo.setDate(monthAgo.getDate() - 30); return { start: monthAgo.getTime(), end: now.getTime(), }; - } - case 'this_week': { - const firstDayOfWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay()); - return { - start: firstDayOfWeek.getTime(), - end: now.getTime(), - }; - } - case 'last_week': { - const firstDayOfLastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay() - 7); - const firstDayOfThisWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay()); - return { - start: firstDayOfLastWeek.getTime(), - end: firstDayOfThisWeek.getTime() - 1, - }; - } - case 'this_month': { - const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - return { - start: firstDayOfMonth.getTime(), - end: now.getTime(), - }; - } - case 'last_month': { - const firstDayOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const firstDayOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1); - return { - start: firstDayOfLastMonth.getTime(), - end: firstDayOfThisMonth.getTime() - 1, - }; - } - case 'this_year': { - const firstDayOfYear = new Date(now.getFullYear(), 0, 1); - return { - start: firstDayOfYear.getTime(), - end: now.getTime(), - }; - } - case 'last_year': { - const firstDayOfLastYear = new Date(now.getFullYear() - 1, 0, 1); - const firstDayOfThisYear = new Date(now.getFullYear(), 0, 1); - return { - start: firstDayOfLastYear.getTime(), - end: firstDayOfThisYear.getTime() - 1, - }; - } - case '12m': { - const twelveMonthsAgo = new Date(now); + } + case 'this_week': { + const firstDayOfWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay()); + return { + start: firstDayOfWeek.getTime(), + end: now.getTime(), + }; + } + case 'last_week': { + const firstDayOfLastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay() - 7); + const firstDayOfThisWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay()); + return { + start: firstDayOfLastWeek.getTime(), + end: firstDayOfThisWeek.getTime() - 1, + }; + } + case 'this_month': { + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + return { + start: firstDayOfMonth.getTime(), + end: now.getTime(), + }; + } + case 'last_month': { + const firstDayOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const firstDayOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1); + return { + start: firstDayOfLastMonth.getTime(), + end: firstDayOfThisMonth.getTime() - 1, + }; + } + case 'this_year': { + const firstDayOfYear = new Date(now.getFullYear(), 0, 1); + return { + start: firstDayOfYear.getTime(), + end: now.getTime(), + }; + } + case 'last_year': { + const firstDayOfLastYear = new Date(now.getFullYear() - 1, 0, 1); + const firstDayOfThisYear = new Date(now.getFullYear(), 0, 1); + return { + start: firstDayOfLastYear.getTime(), + end: firstDayOfThisYear.getTime() - 1, + }; + } + case '12m': { + const twelveMonthsAgo = new Date(now); twelveMonthsAgo.setMonth(twelveMonthsAgo.getMonth() - 12); return { start: twelveMonthsAgo.getTime(), end: now.getTime(), }; - } - case 'all':{ - const start = new Date(app_creation_ts); - return { - start: start.getTime(), - end: now.getTime(), - }; - } - default: - return null; + } + case 'all':{ + const start = new Date(app_creation_ts); + return { + start: start.getTime(), + end: now.getTime(), + }; + } + default: + return null; } }; @@ -796,7 +797,7 @@ class AppInformationService extends BaseService { .filter(Boolean) .map(ext => AppRedisCacheSpace.associationAppsKey(ext)); if ( associationKeys.length ) { - await redisClient.del(...associationKeys); + await deleteRedisKeys(associationKeys); } // remove from recent @@ -836,29 +837,29 @@ class AppInformationService extends BaseService { while ( currentDate <= endDate ) { let period; switch ( grouping ) { - case 'hour': - period = `${currentDate.toISOString().slice(0, 13) }:00:00`; + case 'hour': + period = `${currentDate.toISOString().slice(0, 13) }:00:00`; currentDate.setHours(currentDate.getHours() + 1); - break; - case 'day': - period = currentDate.toISOString().slice(0, 10); + break; + case 'day': + period = currentDate.toISOString().slice(0, 10); currentDate.setDate(currentDate.getDate() + 1); - break; - case 'week': { + break; + case 'week': { // Get the ISO week number - const weekNum = String(this.getWeekNumber(currentDate)).padStart(2, '0'); - period = `${currentDate.getFullYear()}-${weekNum}`; + const weekNum = String(this.getWeekNumber(currentDate)).padStart(2, '0'); + period = `${currentDate.getFullYear()}-${weekNum}`; currentDate.setDate(currentDate.getDate() + 7); break; - } - case 'month': - period = currentDate.toISOString().slice(0, 7); + } + case 'month': + period = currentDate.toISOString().slice(0, 7); currentDate.setMonth(currentDate.getMonth() + 1); - break; - case 'year': - period = currentDate.getFullYear().toString(); + break; + case 'year': + period = currentDate.getFullYear().toString(); currentDate.setFullYear(currentDate.getFullYear() + 1); - break; + break; } periods.push({ period, count: 0 }); } @@ -887,23 +888,23 @@ class AppInformationService extends BaseService { // For ClickHouse results, convert the timestamp to match the expected format if ( item.period instanceof Date ) { switch ( stats_grouping ) { - case 'hour': - period = `${item.period.toISOString().slice(0, 13) }:00:00`; - break; - case 'day': - period = item.period.toISOString().slice(0, 10); - break; - case 'week': { - const weekNum = String(this.getWeekNumber(item.period)).padStart(2, '0'); - period = `${item.period.getFullYear()}-${weekNum}`; - break; - } - case 'month': - period = item.period.toISOString().slice(0, 7); - break; - case 'year': - period = item.period.getFullYear().toString(); - break; + case 'hour': + period = `${item.period.toISOString().slice(0, 13) }:00:00`; + break; + case 'day': + period = item.period.toISOString().slice(0, 10); + break; + case 'week': { + const weekNum = String(this.getWeekNumber(item.period)).padStart(2, '0'); + period = `${item.period.getFullYear()}-${weekNum}`; + break; + } + case 'month': + period = item.period.toISOString().slice(0, 7); + break; + case 'year': + period = item.period.getFullYear().toString(); + break; } } return [period, parseInt(item.count)]; diff --git a/src/backend/src/modules/apps/AppRedisCacheSpace.js b/src/backend/src/modules/apps/AppRedisCacheSpace.js index fbc5904b7..0b44faf29 100644 --- a/src/backend/src/modules/apps/AppRedisCacheSpace.js +++ b/src/backend/src/modules/apps/AppRedisCacheSpace.js @@ -17,6 +17,7 @@ * along with this program. If not, see . */ import { redisClient } from '../../clients/redis/redisSingleton.js'; +import { deleteRedisKeys } from '../../clients/redis/deleteRedisKeys.js'; const appFullNamespace = 'apps'; const appLiteNamespace = 'apps:lite'; @@ -101,7 +102,7 @@ export const AppRedisCacheSpace = { keys.push(...AppRedisCacheSpace.statsKeys(app.uid)); } if ( keys.length ) { - await redisClient.del(...keys); + await deleteRedisKeys(keys); } }, invalidateCachedAppName: async (name, { rawIconVariants = [true, false] } = {}) => { @@ -111,10 +112,10 @@ export const AppRedisCacheSpace = { value: name, rawIcon, })); - await redisClient.del(...keys); + await deleteRedisKeys(keys); }, invalidateAppStats: async (uid) => { if ( ! uid ) return; - await redisClient.del(...AppRedisCacheSpace.statsKeys(uid)); + await deleteRedisKeys(AppRedisCacheSpace.statsKeys(uid)); }, -}; \ No newline at end of file +}; diff --git a/src/backend/src/modules/data-access/AppService.js b/src/backend/src/modules/data-access/AppService.js index db4f1a87e..6e1e0d95a 100644 --- a/src/backend/src/modules/data-access/AppService.js +++ b/src/backend/src/modules/data-access/AppService.js @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import APIError from '../../api/APIError.js'; -import { redisClient } from '../../clients/redis/redisSingleton.js'; +import { deleteRedisKeys } from '../../clients/redis/deleteRedisKeys.js'; import config from '../../config.js'; import { AppRedisCacheSpace } from '../apps/AppRedisCacheSpace.js'; import { NodeInternalIDSelector } from '../../filesystem/node/selectors.js'; @@ -1166,7 +1166,7 @@ export default class AppService extends BaseService { if ( ! normalizedNew.length ) { const affectedExtensions = new Set(normalizedOld); if ( affectedExtensions.size ) { - await redisClient.del(...Array.from(affectedExtensions) + await deleteRedisKeys(Array.from(affectedExtensions) .map(ext => AppRedisCacheSpace.associationAppsKey(ext))); } return; @@ -1180,7 +1180,7 @@ export default class AppService extends BaseService { const affectedExtensions = new Set([...normalizedOld, ...normalizedNew]); if ( affectedExtensions.size ) { - await redisClient.del(...Array.from(affectedExtensions) + await deleteRedisKeys(Array.from(affectedExtensions) .map(ext => AppRedisCacheSpace.associationAppsKey(ext))); } } diff --git a/src/backend/src/om/entitystorage/AppES.js b/src/backend/src/om/entitystorage/AppES.js index 9134574a6..311d966ec 100644 --- a/src/backend/src/om/entitystorage/AppES.js +++ b/src/backend/src/om/entitystorage/AppES.js @@ -18,7 +18,7 @@ */ const APIError = require('../../api/APIError'); const { AppRedisCacheSpace } = require('../../modules/apps/AppRedisCacheSpace.js'); -const { redisClient } = require('../../clients/redis/redisSingleton'); +const { deleteRedisKeys } = require('../../clients/redis/deleteRedisKeys.js'); const config = require('../../config'); const { app_name_exists } = require('../../helpers'); const { AppUnderUserActorType } = require('../../services/auth/Actor'); @@ -181,7 +181,7 @@ class AppES extends BaseES { ...normalizedNewAssociations, ]); if ( affectedAssociationExtensions.size ) { - await redisClient.del(...Array.from(affectedAssociationExtensions) + await deleteRedisKeys(Array.from(affectedAssociationExtensions) .map(ext => AppRedisCacheSpace.associationAppsKey(ext))); } diff --git a/src/backend/src/services/UserRedisCacheSpace.js b/src/backend/src/services/UserRedisCacheSpace.js index d0c6283b6..1195a5912 100644 --- a/src/backend/src/services/UserRedisCacheSpace.js +++ b/src/backend/src/services/UserRedisCacheSpace.js @@ -17,6 +17,7 @@ * along with this program. If not, see . */ import { redisClient } from '../clients/redis/redisSingleton.js'; +import { deleteRedisKeys } from '../clients/redis/deleteRedisKeys.js'; const userKeyPrefix = 'users'; const defaultUserIdProperties = ['username', 'uuid', 'email', 'id', 'referral_code']; @@ -65,7 +66,7 @@ const UserRedisCacheSpace = { invalidateUser: async (user, props = defaultUserIdProperties) => { const keys = UserRedisCacheSpace.keysForUser(user, props); if ( keys.length ) { - await redisClient.del(...keys); + await deleteRedisKeys(keys); } }, invalidateById: async (id, props = defaultUserIdProperties) => { diff --git a/src/backend/src/services/auth/PermissionService.js b/src/backend/src/services/auth/PermissionService.js index bf1d0f9c3..c80be97c6 100644 --- a/src/backend/src/services/auth/PermissionService.js +++ b/src/backend/src/services/auth/PermissionService.js @@ -28,6 +28,7 @@ const { UserActorType, Actor, AppUnderUserActorType } = require('./Actor'); const { PERM_KEY_PREFIX, MANAGE_PERM_PREFIX } = require('./permissionConts.mjs'); const { PermissionUtil, PermissionExploder, PermissionImplicator, PermissionRewriter } = require('./permissionUtils.mjs'); const { spanify } = require('../../util/otelutil'); +const { deleteRedisKeys } = require('../../clients/redis/deleteRedisKeys.js'); const { redisClient } = require('../../clients/redis/redisSingleton'); const { PermissionScanRedisCacheSpace } = require('./PermissionScanRedisCacheSpace.js'); const { Context } = require('../../util/context'); @@ -277,7 +278,7 @@ class PermissionService extends BaseService { cursor = next_cursor; if ( keys?.length ) toDelete.push(...keys); } while ( cursor !== '0' ); - if ( toDelete.length ) await redisClient.del(...toDelete); + if ( toDelete.length ) await deleteRedisKeys(toDelete); } async validateUserPerms ({ actor, permissions }) {