82 lines
3.7 KiB
TypeScript
82 lines
3.7 KiB
TypeScript
// How long a saved read stays fresh, which store lanes a read uses, and the reads the background reader does for
|
|
// every app. Pure (tests/scheduler.test.ts).
|
|
import type { Lane } from './scheduler';
|
|
|
|
const MIN = 60_000;
|
|
|
|
/** Time to live of a saved read per method: status and submission state 5 min, reviews 15 min, catalog / prices /
|
|
* listings 30 min, health and analytics 6 h (the stores update them daily). */
|
|
export const TTL: Record<string, number> = {
|
|
'portfolio.status': 5 * MIN,
|
|
'apple.status': 5 * MIN,
|
|
'play.status': 5 * MIN,
|
|
'apple.testflight_status': 5 * MIN,
|
|
'reviews.list': 15 * MIN,
|
|
'catalog.diff': 30 * MIN,
|
|
'prices.current': 30 * MIN,
|
|
'project.listings': 30 * MIN,
|
|
'project.editable': 30 * MIN,
|
|
'listing.live': 30 * MIN,
|
|
'art.live': 6 * 60 * MIN,
|
|
'apple.ppo_status': 30 * MIN,
|
|
validate: 30 * MIN,
|
|
scan: 30 * MIN,
|
|
health: 6 * 60 * MIN,
|
|
'health.issue': 6 * 60 * MIN,
|
|
'analytics.compare': 6 * 60 * MIN,
|
|
'aso.audit': 6 * 60 * MIN,
|
|
};
|
|
export const DEFAULT_TTL = 30 * MIN;
|
|
|
|
export const ttlOf = (method: string): number => TTL[method] ?? DEFAULT_TTL;
|
|
|
|
/** True when a read saved at `at` (ISO) is older than its TTL, or there is none. */
|
|
export function isStale(method: string, at: string | null | undefined, now = Date.now()): boolean {
|
|
if (!at) return true;
|
|
const t = Date.parse(at);
|
|
return !Number.isFinite(t) || now - t >= ttlOf(method);
|
|
}
|
|
|
|
/** The store lanes a read takes a slot in. */
|
|
export function lanesOf(method: string, o: { ios: boolean; android: boolean }): Lane[] {
|
|
if (method.startsWith('apple.')) return ['apple'];
|
|
if (method.startsWith('play.')) return ['play'];
|
|
if (['project.listings', 'project.editable', 'scan', 'analytics.compare', 'aso.audit', 'reviews.drafts', 'project.declarations', 'project.renders', 'project.icon'].includes(method)) return ['local'];
|
|
const l: Lane[] = [];
|
|
if (o.ios) l.push('apple');
|
|
if (o.android) l.push('play');
|
|
return l.length ? l : ['local'];
|
|
}
|
|
|
|
export interface PlannedRead { method: string; args: Record<string, unknown> }
|
|
|
|
/** The reads the background reader does for one app (the ones its tabs show), by what the app has. */
|
|
export function readsFor(o: { folder: boolean; ios: boolean; android: boolean }): PlannedRead[] {
|
|
const r: PlannedRead[] = [];
|
|
const add = (method: string, args: Record<string, unknown> = {}) => r.push({ method, args });
|
|
if (o.ios) { add('apple.status'); add('apple.testflight_status'); }
|
|
if (o.android) add('play.status');
|
|
if (o.ios || o.android) { add('reviews.list', { days: 30 }); add('health'); }
|
|
if (o.folder) {
|
|
add('scan'); add('project.listings'); add('project.editable');
|
|
if (o.ios || o.android) { add('catalog.diff'); add('prices.current'); add('validate'); }
|
|
add('analytics.compare'); add('aso.audit');
|
|
}
|
|
if (o.ios) add('apple.ppo_status');
|
|
// store-only apps: the listing editor's live texts
|
|
if (!o.folder && (o.ios || o.android)) add('listing.live');
|
|
return r;
|
|
}
|
|
|
|
/** Key of one read in the scheduler and the read bus. */
|
|
export const readKey = (target: string, method: string, args: Record<string, unknown> = {}): string => `${target}\u001f${method}\u001f${JSON.stringify(args)}`;
|
|
|
|
/** "429", "RESOURCE_EXHAUSTED", "rate limit": the store refused for its quota (not the read itself). */
|
|
export const looksThrottled = (text: string | undefined): boolean => !!text && /\b429\b|RESOURCE_EXHAUSTED|rate.?limit|quota exceeded/i.test(text);
|
|
|
|
/** The one-line reason of a failed read, for "couldn't refresh: <reason>". */
|
|
export function oneLine(text: string, max = 140): string {
|
|
const l = text.trim().split('\n').map(s => s.trim()).filter(Boolean).at(-1) ?? '';
|
|
const s = l.replace(/^\S+ \[[^\]]+\] /, '');
|
|
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
}
|