// 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 } : {}) }; }