dev: move recommended apps to service with cache invalidation

Adds an event for app invalidation.

Moves iconify_apps to AppIconService and adds RecommenededAppsService,
which holds some logic that used to be in get-launch-apps. The event for
app invalidation is then used by RecommenededAppsService to determine
when to clear the recommended apps cache.
This commit is contained in:
KernelDeimos
2024-12-30 12:38:51 -05:00
parent c1961dd54e
commit 055ba7d9df
5 changed files with 171 additions and 81 deletions

View File

@@ -209,6 +209,7 @@ function invalidate_cached_user_by_id (id) {
async function refresh_apps_cache(options, override){
/** @type BaseDatabaseAccessService */
const db = services.get('database').get(DB_READ, 'apps');
const svc_event = services.get('event');
const log = services.get('log-service').create('refresh_apps_cache');
log.tick('refresh apps cache');
@@ -221,6 +222,9 @@ async function refresh_apps_cache(options, override){
kv.set('apps:id:' + app.id, app);
kv.set('apps:uid:' + app.uid, app);
}
svc_event.emit('apps.invalidate', {
options, apps,
});
}
// refresh only apps that are approved for listing
else if(options.only_approved_for_listing){
@@ -231,6 +235,9 @@ async function refresh_apps_cache(options, override){
kv.set('apps:id:' + app.id, app);
kv.set('apps:uid:' + app.uid, app);
}
svc_event.emit('apps.invalidate', {
options, apps,
});
}
// if options is provided, refresh only the app specified
else{
@@ -259,6 +266,10 @@ async function refresh_apps_cache(options, override){
kv.set('apps:id:' + app.id, app);
kv.set('apps:uid:' + app.uid, app);
}
svc_event.emit('apps.invalidate', {
options, app,
});
}
}

View File

@@ -4,7 +4,7 @@ const { LLRead } = require("../../filesystem/ll_operations/ll_read");
const { NodePathSelector } = require("../../filesystem/node/selectors");
const { get_app } = require("../../helpers");
const { Endpoint } = require("../../util/expressutil");
const { buffer_to_stream } = require("../../util/streamutil");
const { buffer_to_stream, stream_to_buffer } = require("../../util/streamutil");
const BaseService = require("../../services/BaseService.js");
const ICON_SIZES = [16,32,64,128,256,512];
@@ -30,6 +30,8 @@ class AppIconService extends BaseService {
bmp: require('sharp-bmp'),
ico: require('sharp-ico'),
}
static ICON_SIZES = ICON_SIZES;
/**
* AppIconService listens to this event to register the
@@ -61,6 +63,37 @@ class AppIconService extends BaseService {
},
}).attach(app);
}
get_sizes () {
return this.constructor.ICON_SIZES;
}
async iconify_apps ({ apps, size }) {
return await Promise.all(apps.map(async app => {
const icon_result = await this.get_icon_stream({
app_icon: app.icon,
app_uid: app.uid ?? app.uuid,
size: size,
});
if ( icon_result.data_url ) {
app.icon = icon_result.data_url;
return app;
}
try {
const buffer = await stream_to_buffer(icon_result.stream);
const resp_data_url = `data:${icon_result.mime};base64,${buffer.toString('base64')}`;
app.icon = resp_data_url;
} catch (e) {
this.errors.report('get-launch-apps:icon-stream', {
source: e,
});
}
return app;
}));
}
async get_icon_stream ({ app_icon, app_uid, size, tries = 0 }) {
// If there is an icon provided, and it's an SVG, we'll just return it

View File

@@ -15,6 +15,9 @@ class AppsModule extends AdvancedBase {
const { ProtectedAppService } = require('./ProtectedAppService');
services.registerService('__protected-app', ProtectedAppService);
const RecommendedAppsService = require('./RecommendedAppsService');
services.registerService('recommended-apps', RecommendedAppsService);
}
}

View File

@@ -0,0 +1,119 @@
const { get_app } = require("../../helpers");
const BaseService = require("../../services/BaseService");
const get_apps = async ({ specifiers }) => {
return await Promise.all(specifiers.map(async (specifier) => {
return await get_app(specifier);
}));
};
class RecommendedAppsService extends BaseService {
static APP_NAMES = [
'app-center',
'dev-center',
'editor',
'code',
'camera',
'recorder',
'shell-shockers-outpan',
'krunker',
'slash-frvr',
'viewer',
'solitaire-frvr',
'terminal',
'tiles-beat',
'draw',
'silex',
'markus',
'puterjs-playground',
'player',
'pdf',
'photopea',
'polotno',
'basketball-frvr',
'gold-digger-frvr',
'plushie-connect',
'hex-frvr',
'spider-solitaire',
'danger-cross',
'doodle-jump-extra',
'endless-lake',
'sword-and-jewel',
'reversi-2',
'in-orbit',
'bowling-king',
'calc-hklocykcpts',
'virtu-piano',
'battleship-war',
'turbo-racing',
'guns-and-bottles',
'tronix',
'jewel-classic',
];
_construct () {
this.app_names = new Set(RecommendedAppsService.APP_NAMES);
}
['__on_boot.consolidation'] () {
const svc_appIcon = this.services.get('app-icon');
const svc_event = this.services.get('event');
svc_event.on('apps.invalidate', (_, { app }) => {
const sizes = svc_appIcon.get_sizes();
this.log.noticeme('Invalidating recommended apps', { app, sizes });
// If it's a single-app invalidation, only invalidate if the
// app is in the list of recommended apps
if ( app ) {
const name = app.name;
if ( ! this.app_names.has(name) ) return;
}
kv.del('global:recommended-apps');
for ( const size of sizes ) {
const key = `global:recommended-apps:icon-size:${size}`;
kv.del(key);
}
});
}
async get_recommended_apps ({ icon_size }) {
const recommended_cache_key = 'global:recommended-apps' + (
icon_size ? `:icon-size:${icon_size}` : ''
);
let recommended = kv.get(recommended_cache_key);
if ( recommended ) return recommended;
// Prepare each app for returning to user by only returning the necessary fields
// and adding them to the retobj array
recommended = (await get_apps({
specifiers: Array.from(this.app_names).map(name => ({ name }))
})).filter(app => !! app).map(app => {
return {
uuid: app.uid,
name: app.name,
title: app.title,
icon: app.icon,
godmode: app.godmode,
maximize_on_start: app.maximize_on_start,
index_url: app.index_url,
};
});
const svc_appIcon = this.services.get('app-icon');
// Iconify apps
if ( icon_size ) {
recommended = await svc_appIcon.iconify_apps({
apps: recommended,
size: icon_size,
});
}
kv.set(recommended_cache_key, recommended);
}
}
module.exports = RecommendedAppsService;

View File

@@ -24,12 +24,6 @@ const { get_app } = require('../helpers.js');
const { DB_READ } = require('../services/database/consts.js');
const { stream_to_buffer } = require('../util/streamutil.js');
const get_apps = async ({ specifiers }) => {
return await Promise.all(specifiers.map(async (specifier) => {
return await get_app(specifier);
}));
};
const iconify_apps = async (context, { apps, size }) => {
return await Promise.all(apps.map(async app => {
const svc_appIcon = context.services.get('app-icon');
@@ -77,80 +71,10 @@ module.exports = async (req, res) => {
// -----------------------------------------------------------------------//
// Recommended apps
// -----------------------------------------------------------------------//
const recommended_cache_key = 'global:recommended-apps' + (
req.query.icon_size ? `:icon-size:${req.query.icon_size}` : ''
);
result.recommended = kv.get(recommended_cache_key);
if ( ! result.recommended ) {
let app_names = new Set([
'app-center',
'dev-center',
'editor',
'code',
'camera',
'recorder',
'shell-shockers-outpan',
'krunker',
'slash-frvr',
'viewer',
'solitaire-frvr',
'terminal',
'tiles-beat',
'draw',
'silex',
'markus',
'puterjs-playground',
'player',
'pdf',
'photopea',
'polotno',
'basketball-frvr',
'gold-digger-frvr',
'plushie-connect',
'hex-frvr',
'spider-solitaire',
'danger-cross',
'doodle-jump-extra',
'endless-lake',
'sword-and-jewel',
'reversi-2',
'in-orbit',
'bowling-king',
'calc-hklocykcpts',
'virtu-piano',
'battleship-war',
'turbo-racing',
'guns-and-bottles',
'tronix',
'jewel-classic',
]);
// Prepare each app for returning to user by only returning the necessary fields
// and adding them to the retobj array
result.recommended = (await get_apps({
specifiers: Array.from(app_names).map(name => ({ name }))
})).filter(app => !! app).map(app => {
return {
uuid: app.uid,
name: app.name,
title: app.title,
icon: app.icon,
godmode: app.godmode,
maximize_on_start: app.maximize_on_start,
index_url: app.index_url,
};
});
// Iconify apps
if ( req.query.icon_size ) {
result.recommended = await iconify_apps({ services: req.services }, {
apps: result.recommended,
size: req.query.icon_size,
});
}
kv.set(recommended_cache_key, result.recommended);
}
const svc_recommendedApps = req.services.get('recommended-apps');
result.recommended = await svc_recommendedApps.get_recommended_apps({
icon_size: req.query.icon_size
});
// -----------------------------------------------------------------------//
// Recent apps