desktop reads everything by itself: one background read scheduler (4 per store, starts spaced per account, App Store Connect X-Rate-Limit slows it below 20 % and pauses at 100 left, backoff with jitter on 429 / RESOURCE_EXHAUSTED, visible app first, one read per key), per-method TTLs over the saved reads, Settings auto-read (on) and interval (15 min, paused while hidden), Refresh all, a refresh icon and 'read 3 min ago' / 'couldn't refresh' per card, writes invalidate, status line with progress and quota; quota reaches the window through ToolResult.quota (cli/lib/quota.ts); Health top issues open with versions, sample stack (health --issue / --signature, read health_issue), Open in Play Console, Copy for AI and likely-cause hints (cli/lib/crash-hints.ts); tests/scheduler.test.ts, tests/quota-crash-hints.test.ts, boot check auto-read case
This commit is contained in:
parent
89256f1e80
commit
0fb4ce0001
46 changed files with 1531 additions and 141 deletions
77
apps/desktop/src/lib/freshness.ts
Normal file
77
apps/desktop/src/lib/freshness.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// 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,
|
||||
'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', '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');
|
||||
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');
|
||||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue