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 }) {