mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-03 16:10:31 +00:00
Add app object Redis cache and use in AppES
This commit is contained in:
@@ -21,6 +21,7 @@ import { deleteRedisKeys } from '../../clients/redis/deleteRedisKeys.js';
|
||||
|
||||
const appFullNamespace = 'apps';
|
||||
const appLookupKeys = ['uid', 'name', 'id'];
|
||||
const appObjectSuffix = 'object';
|
||||
|
||||
const safeParseJson = (value, fallback = null) => {
|
||||
if ( value === null || value === undefined ) return fallback;
|
||||
@@ -45,15 +46,29 @@ const appCacheKey = ({ lookup, value }) => (
|
||||
`${appNamespace()}:${lookup}:${value}`
|
||||
);
|
||||
|
||||
const appObjectNamespace = () => `${appNamespace()}:${appObjectSuffix}`;
|
||||
|
||||
const appObjectCacheKey = ({ lookup, value }) => (
|
||||
`${appObjectNamespace()}:${lookup}:${value}`
|
||||
);
|
||||
|
||||
export const AppRedisCacheSpace = {
|
||||
key: appCacheKey,
|
||||
namespace: appNamespace,
|
||||
objectNamespace: appObjectNamespace,
|
||||
objectKey: appObjectCacheKey,
|
||||
keysForApp: (app) => {
|
||||
if ( ! app ) return [];
|
||||
return appLookupKeys
|
||||
.filter(lookup => app[lookup] !== undefined && app[lookup] !== null && app[lookup] !== '')
|
||||
.map(lookup => appCacheKey({ lookup, value: app[lookup] }));
|
||||
},
|
||||
objectKeysForApp: (app) => {
|
||||
if ( ! app ) return [];
|
||||
return appLookupKeys
|
||||
.filter(lookup => app[lookup] !== undefined && app[lookup] !== null && app[lookup] !== '')
|
||||
.map(lookup => appObjectCacheKey({ lookup, value: app[lookup] }));
|
||||
},
|
||||
uidScanPattern: () => `${appNamespace()}:uid:*`,
|
||||
pendingNamespace: () => 'pending_app',
|
||||
pendingKey: ({ lookup, value }) => (
|
||||
@@ -77,6 +92,9 @@ export const AppRedisCacheSpace = {
|
||||
getCachedApp: async ({ lookup, value }) => (
|
||||
safeParseJson(await redisClient.get(appCacheKey({ lookup, value })))
|
||||
),
|
||||
getCachedAppObject: async ({ lookup, value }) => (
|
||||
safeParseJson(await redisClient.get(appObjectCacheKey({ lookup, value })))
|
||||
),
|
||||
setCachedApp: async (app, { ttlSeconds } = {}) => {
|
||||
if ( ! app ) return;
|
||||
const serialized = JSON.stringify(app);
|
||||
@@ -86,9 +104,21 @@ export const AppRedisCacheSpace = {
|
||||
await Promise.all(writes);
|
||||
}
|
||||
},
|
||||
setCachedAppObject: async (app, { ttlSeconds } = {}) => {
|
||||
if ( ! app ) return;
|
||||
const serialized = JSON.stringify(app);
|
||||
const writes = AppRedisCacheSpace.objectKeysForApp(app)
|
||||
.map(key => setKey(key, serialized, { ttlSeconds: ttlSeconds || 60 }));
|
||||
if ( writes.length ) {
|
||||
await Promise.all(writes);
|
||||
}
|
||||
},
|
||||
invalidateCachedApp: (app, { includeStats = false } = {}) => {
|
||||
if ( ! app ) return;
|
||||
const keys = [...AppRedisCacheSpace.keysForApp(app)];
|
||||
const keys = [
|
||||
...AppRedisCacheSpace.keysForApp(app),
|
||||
...AppRedisCacheSpace.objectKeysForApp(app),
|
||||
];
|
||||
if ( includeStats && app.uid ) {
|
||||
keys.push(...AppRedisCacheSpace.statsKeys(app.uid));
|
||||
}
|
||||
@@ -98,10 +128,16 @@ export const AppRedisCacheSpace = {
|
||||
},
|
||||
invalidateCachedAppName: async (name) => {
|
||||
if ( ! name ) return;
|
||||
const keys = [appCacheKey({
|
||||
lookup: 'name',
|
||||
value: name,
|
||||
})];
|
||||
const keys = [
|
||||
appCacheKey({
|
||||
lookup: 'name',
|
||||
value: name,
|
||||
}),
|
||||
appObjectCacheKey({
|
||||
lookup: 'name',
|
||||
value: name,
|
||||
}),
|
||||
];
|
||||
return deleteRedisKeys(keys);
|
||||
},
|
||||
invalidateAppStats: async (uid) => {
|
||||
|
||||
@@ -33,6 +33,7 @@ const uuidv4 = require('uuid').v4;
|
||||
const APP_UID_ALIAS_KEY_PREFIX = 'app:canonicalUidAlias';
|
||||
const APP_UID_ALIAS_REVERSE_KEY_PREFIX = 'app:canonicalUidAliasReverse';
|
||||
const APP_UID_ALIAS_TTL_SECONDS = 60 * 60 * 24 * 90;
|
||||
const APP_OBJECT_CACHE_TTL_SECONDS = 24 * 60 * 60;
|
||||
const indexUrlUniquenessExemptionCandidates = [
|
||||
'https://dev-center.puter.com/coming-soon',
|
||||
];
|
||||
@@ -446,6 +447,26 @@ class AppES extends BaseES {
|
||||
});
|
||||
},
|
||||
|
||||
async get_cached_app_object_ (appUid) {
|
||||
if ( typeof appUid !== 'string' || !appUid ) return null;
|
||||
return await AppRedisCacheSpace.getCachedAppObject({
|
||||
lookup: 'uid',
|
||||
value: appUid,
|
||||
});
|
||||
},
|
||||
|
||||
async set_cached_app_object_ (entity) {
|
||||
if ( ! entity ) return;
|
||||
|
||||
const cacheable = await entity.get_client_safe();
|
||||
delete cacheable.stats;
|
||||
delete cacheable.privateAccess;
|
||||
|
||||
await AppRedisCacheSpace.setCachedAppObject(cacheable, {
|
||||
ttlSeconds: APP_OBJECT_CACHE_TTL_SECONDS,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms app data before reading by adding associations and handling permissions
|
||||
* @param {Object} entity - App entity to transform
|
||||
@@ -463,6 +484,7 @@ class AppES extends BaseES {
|
||||
const appIndexUrl = await entity.get('index_url');
|
||||
const appCreatedAt = await entity.get('created_at');
|
||||
const appIsPrivate = await entity.get('is_private');
|
||||
const cachedAppObject = await this.get_cached_app_object_(appUid);
|
||||
|
||||
const appInformationService = services.get('app-information');
|
||||
const authService = services.get('auth');
|
||||
@@ -473,21 +495,36 @@ class AppES extends BaseES {
|
||||
created_at: appCreatedAt,
|
||||
})
|
||||
: Promise.resolve(undefined);
|
||||
const fileAssociationsPromise = this.db.read(
|
||||
'SELECT type FROM app_filetype_association WHERE app_id = ?',
|
||||
[entity.private_meta.mysql_id],
|
||||
const cachedFiletypeAssociations = Array.isArray(cachedAppObject?.filetype_associations)
|
||||
? cachedAppObject.filetype_associations
|
||||
: null;
|
||||
const hasCachedCreatedFromOrigin = !!(
|
||||
cachedAppObject &&
|
||||
Object.prototype.hasOwnProperty.call(cachedAppObject, 'created_from_origin')
|
||||
);
|
||||
const createdFromOriginPromise = (async () => {
|
||||
if ( ! authService ) return null;
|
||||
try {
|
||||
const origin = origin_from_url(appIndexUrl);
|
||||
const expectedUid = await authService.app_uid_from_origin(origin);
|
||||
return expectedUid === appUid ? origin : null;
|
||||
} catch {
|
||||
// This happens when index_url is not a valid URL.
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const shouldRefreshCachedAppObject =
|
||||
!cachedAppObject ||
|
||||
!cachedFiletypeAssociations ||
|
||||
!hasCachedCreatedFromOrigin;
|
||||
const fileAssociationsPromise = cachedFiletypeAssociations
|
||||
? Promise.resolve(cachedFiletypeAssociations)
|
||||
: this.db.read(
|
||||
'SELECT type FROM app_filetype_association WHERE app_id = ?',
|
||||
[entity.private_meta.mysql_id],
|
||||
).then(rows => rows.map(row => row.type));
|
||||
const createdFromOriginPromise = hasCachedCreatedFromOrigin
|
||||
? Promise.resolve(cachedAppObject.created_from_origin ?? null)
|
||||
: (async () => {
|
||||
if ( ! authService ) return null;
|
||||
try {
|
||||
const origin = origin_from_url(appIndexUrl);
|
||||
const expectedUid = await authService.app_uid_from_origin(origin);
|
||||
return expectedUid === appUid ? origin : null;
|
||||
} catch {
|
||||
// This happens when index_url is not a valid URL.
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const privateAccessPromise = resolvePrivateLaunchAccess({
|
||||
app: {
|
||||
uid: appUid,
|
||||
@@ -501,7 +538,7 @@ class AppES extends BaseES {
|
||||
});
|
||||
|
||||
const [
|
||||
fileAssociationRows,
|
||||
filetypeAssociations,
|
||||
stats,
|
||||
createdFromOrigin,
|
||||
privateAccess,
|
||||
@@ -511,13 +548,13 @@ class AppES extends BaseES {
|
||||
createdFromOriginPromise,
|
||||
privateAccessPromise,
|
||||
]);
|
||||
await entity.set(
|
||||
'filetype_associations',
|
||||
fileAssociationRows.map(row => row.type),
|
||||
);
|
||||
await entity.set('filetype_associations', filetypeAssociations);
|
||||
await entity.set('stats', stats);
|
||||
await entity.set('created_from_origin', createdFromOrigin);
|
||||
await entity.set('privateAccess', privateAccess);
|
||||
if ( shouldRefreshCachedAppObject ) {
|
||||
await this.set_cached_app_object_(entity);
|
||||
}
|
||||
|
||||
// Migrate b64 icons to the filesystem-backed icon flow without blocking reads.
|
||||
this.queueIconMigration(entity);
|
||||
|
||||
Reference in New Issue
Block a user