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
51
cli/lib/quota.ts
Normal file
51
cli/lib/quota.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Store API quota seen by a command, passed to `storebound serve` (the desktop's read scheduler) as one marked stderr
|
||||
// line per change: App Store Connect's X-Rate-Limit header ("user-hour-lim:3600;user-hour-rem:3412;") and 429 /
|
||||
// RESOURCE_EXHAUSTED answers of the Google APIs. Outside serve (STOREBOUND_MACHINE unset) nothing is printed.
|
||||
|
||||
/** Prefix of the quota line on stderr; runner.ts takes it out of the output. */
|
||||
export const QUOTA_MARK = '\u001estorebound-quota ';
|
||||
|
||||
export interface QuotaInfo {
|
||||
store: 'apple' | 'play';
|
||||
/** Requests per hour of this key (App Store Connect). */
|
||||
limit?: number;
|
||||
/** Requests left in the current hour. */
|
||||
remaining?: number;
|
||||
/** The store answered 429 / RESOURCE_EXHAUSTED at least once in this run. */
|
||||
throttled?: boolean;
|
||||
}
|
||||
|
||||
/** "user-hour-lim:3600;user-hour-rem:3412;" -> { limit: 3600, remaining: 3412 }; null when it is not there. */
|
||||
export function parseAscRateLimit(header: string | null | undefined): { limit: number; remaining: number } | null {
|
||||
if (!header) return null;
|
||||
const lim = /user-hour-lim:(\d+)/i.exec(header), rem = /user-hour-rem:(\d+)/i.exec(header);
|
||||
return lim && rem ? { limit: Number(lim[1]), remaining: Number(rem[1]) } : null;
|
||||
}
|
||||
|
||||
let last = '';
|
||||
function emit(q: QuotaInfo): void {
|
||||
if (process.env.STOREBOUND_MACHINE !== '1') return;
|
||||
const line = JSON.stringify(q);
|
||||
if (line === last) return;
|
||||
last = line;
|
||||
process.stderr.write(QUOTA_MARK + line + '\n');
|
||||
}
|
||||
|
||||
/** After every App Store Connect answer. */
|
||||
export function noteAsc(r: { status: number; headers: { get(n: string): string | null } }): void {
|
||||
const rl = parseAscRateLimit(r.headers.get('x-rate-limit'));
|
||||
if (rl) emit({ store: 'apple', ...rl, ...(r.status === 429 ? { throttled: true } : {}) });
|
||||
else if (r.status === 429) emit({ store: 'apple', throttled: true });
|
||||
}
|
||||
|
||||
/** After every Google answer (Play Developer API, Reporting API). */
|
||||
export function noteGoogle(status: number): void {
|
||||
if (status === 429) emit({ store: 'play', throttled: true });
|
||||
}
|
||||
|
||||
/** Folds the marked lines of one run into one value (the latest counts; throttled if any line said so). */
|
||||
export function mergeQuota(a: QuotaInfo | undefined, b: QuotaInfo): QuotaInfo {
|
||||
if (!a || a.store !== b.store) return b;
|
||||
const remaining = b.remaining ?? a.remaining, limit = b.limit ?? a.limit;
|
||||
return { store: b.store, ...(limit === undefined ? {} : { limit }), ...(remaining === undefined ? {} : { remaining }), ...(a.throttled || b.throttled ? { throttled: true } : {}) };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue