refactor: merge apple/google app caches into one parametrized repo

Two near-identical per-store repository files collapse into a single
createAppCacheRepository(handle, prefix, ttl) factory used twice from
app-builder. Scrapers now receive the cache directly (one less layer),
and the cache entry shape drops the obsolete _id field — the Redis key
already encodes the appId.
This commit is contained in:
2026-05-09 22:32:56 +07:00
parent a2016eca18
commit 55cdc04c57
6 changed files with 49 additions and 86 deletions
+10 -15
View File
@@ -10,40 +10,35 @@ export function buildAppleRequestByBundleId(appId, country) {
return { appId, country, ratings: true };
}
export function createAppleScraper(config, repository) {
export function createAppleScraper(config, cache) {
const { logger } = config;
const repo = repository.appleApp;
async function app(req) {
return store.app(req);
}
async function rawApp(req) {
return JSON.stringify(await app(req));
return JSON.stringify(await store.app(req));
}
async function cache(resp) {
async function saveCache(resp) {
if (!resp || !resp.appId) return;
try {
await repo.save({ _id: resp.appId, app: resp, millis: Date.now() });
await cache.save(resp.appId, resp);
} catch (err) {
logger.warn({ appId: resp.appId, err: err.message }, 'failed to cache apple app');
}
}
async function getApp(appId, country) {
const cached = await repo.getCached(appId);
const cached = await cache.getCached(appId);
if (cached) return cached.app;
const resp = await app(buildAppleRequestByBundleId(appId, country));
await cache(resp);
const resp = await store.app(buildAppleRequestByBundleId(appId, country));
await saveCache(resp);
return resp;
}
async function fetchAndCache(req) {
const resp = await app(req);
await cache(resp);
const resp = await store.app(req);
await saveCache(resp);
return resp;
}
return { rawApp, app, getApp, fetchAndCache };
return { rawApp, getApp, fetchAndCache };
}
+10 -15
View File
@@ -6,42 +6,37 @@ export function buildGoogleRequest(appId, country) {
return { appId, country: country || 'vn' };
}
export function createGoogleScraper(config, repository) {
export function createGoogleScraper(config, cache) {
const { logger } = config;
const repo = repository.googleApp;
async function app(req) {
return gplay.app(req);
}
async function rawApp(req) {
return JSON.stringify(await app(req));
return JSON.stringify(await gplay.app(req));
}
async function cache(resp, fallbackId) {
async function saveCache(resp, fallbackId) {
if (!resp) return;
const id = resp.appId || fallbackId;
if (!id) return;
try {
await repo.save({ _id: id, app: resp, millis: Date.now() });
await cache.save(id, resp);
} catch (err) {
logger.warn({ appId: id, err: err.message }, 'failed to cache google app');
}
}
async function getApp(appId, country) {
const cached = await repo.getCached(appId);
const cached = await cache.getCached(appId);
if (cached) return cached.app;
const resp = await app(buildGoogleRequest(appId, country));
await cache(resp, appId);
const resp = await gplay.app(buildGoogleRequest(appId, country));
await saveCache(resp, appId);
return resp;
}
async function fetchAndCache(req) {
const resp = await app(req);
await cache(resp, req.appId);
const resp = await gplay.app(req);
await saveCache(resp, req.appId);
return resp;
}
return { rawApp, app, getApp, fetchAndCache };
return { rawApp, getApp, fetchAndCache };
}
+5 -6
View File
@@ -6,8 +6,7 @@ import { loadConfig } from './config.js';
import { createUpstashClient } from './repository/upstash.js';
import { createAdminRepository } from './repository/admin-repository.js';
import { createGroupRepository } from './repository/group-repository.js';
import { createAppleAppRepository } from './repository/apple-app-repository.js';
import { createGoogleAppRepository } from './repository/google-app-repository.js';
import { createAppCacheRepository } from './repository/app-cache-repository.js';
import { createAppleScraper } from './api/apple-scraper.js';
import { createGoogleScraper } from './api/google-scraper.js';
import { createBot } from './bot/bot.js';
@@ -18,11 +17,11 @@ export function buildApp(env) {
const store = {
admin: createAdminRepository(handle),
group: createGroupRepository(handle),
appleApp: createAppleAppRepository(handle, config.appCacheSeconds),
googleApp: createGoogleAppRepository(handle, config.appCacheSeconds),
};
const appleScraper = createAppleScraper(config, store);
const googleScraper = createGoogleScraper(config, store);
const appleCache = createAppCacheRepository(handle, 'apple', config.appCacheSeconds);
const googleCache = createAppCacheRepository(handle, 'google', config.appCacheSeconds);
const appleScraper = createAppleScraper(config, appleCache);
const googleScraper = createGoogleScraper(config, googleCache);
const { sender, commands } = createBot(config, store, appleScraper, googleScraper);
return { config, store, appleScraper, googleScraper, sender, commands };
}
+24
View File
@@ -0,0 +1,24 @@
import { getJson, putJson } from './upstash.js';
// Per-store cache for upstream app responses. TTL via Redis EX —
// expired keys are deleted, so a null read is the cache miss.
// `prefix` is the logical key namespace ('apple' or 'google').
export function createAppCacheRepository(handle, prefix, appCacheSeconds) {
function key(appId) {
return `${prefix}:${appId}`;
}
return {
async getCached(appId) {
return getJson(handle, key(appId));
},
async save(appId, response) {
await putJson(
handle,
key(appId),
{ app: response, millis: Date.now() },
{ expirationTtl: appCacheSeconds },
);
},
};
}
-25
View File
@@ -1,25 +0,0 @@
import { getJson, putJson } from './upstash.js';
// Upstash-backed Apple app cache. Logical key shape: `apple:{appId}`.
// Redis EX (via expirationTtl) replaces Java/Mongo's manual
// `(now - millis) > cacheMillis` check — expired keys are deleted, so a
// get() returning null is the cache miss.
export function createAppleAppRepository(handle, appCacheSeconds) {
function key(appId) {
return `apple:${appId}`;
}
async function get(appId) {
return getJson(handle, key(appId));
}
async function save(entry) {
await putJson(handle, key(entry._id), entry, { expirationTtl: appCacheSeconds });
}
async function getCached(appId) {
return get(appId);
}
return { get, save, getCached };
}
-25
View File
@@ -1,25 +0,0 @@
import { getJson, putJson } from './upstash.js';
// Upstash-backed Google app cache. Logical key shape: `google:{appId}`.
// Redis EX (via expirationTtl) replaces Java/Mongo's manual
// `(now - millis) > cacheMillis` check — expired keys are deleted, so a
// get() returning null is the cache miss.
export function createGoogleAppRepository(handle, appCacheSeconds) {
function key(appId) {
return `google:${appId}`;
}
async function get(appId) {
return getJson(handle, key(appId));
}
async function save(entry) {
await putJson(handle, key(entry._id), entry, { expirationTtl: appCacheSeconds });
}
async function getCached(appId) {
return get(appId);
}
return { get, save, getCached };
}