import { RemoteConfig, RemoteConfigBase, SourceConfig } from "../IArchives"; export const INTERMEDIATE_EXPIRE_GRACE = 15 * 60 * 1000; const INTERMEDIATE_VERSION_FRACTION = 1_000_000; /** Adding or removing intermediates is a real config update, so it takes a real version increment - but a proportional one, so it stays far below whatever the author's next version would be (whether they count 1, 2, 3 or use timestamps), and a million of them still fit under it. */ export function nextIntermediateVersion(version: number): number { let step = Math.abs(version) / INTERMEDIATE_VERSION_FRACTION; if (!(step > 0)) { step = 1 / INTERMEDIATE_VERSION_FRACTION; } return version + step; } function isSourceConfig(source: RemoteConfigBase): source is SourceConfig { return typeof source !== "string"; } function cloneSource(source: RemoteConfigBase): RemoteConfigBase { if (!isSourceConfig(source)) return source; return { ...source, validWindow: [source.validWindow[0], source.validWindow[1]] }; } function joinKey(source: SourceConfig): string { return JSON.stringify({ ...source, validWindow: undefined }); } export function getIntermediateSources(config: RemoteConfig): SourceConfig[] { return config.sources.filter(x => isSourceConfig(x) && x.intermediate) as SourceConfig[]; } export function hasIntermediateSources(config: RemoteConfig): boolean { return config.sources.some(x => isSourceConfig(x) && x.intermediate); } /** Removes every intermediate entry and rejoins the windows it split, giving back the underlying configuration. Two configs that resolve equal differ only by intermediates. */ export function resolveIntermediateSources(config: RemoteConfig): RemoteConfig { if (!hasIntermediateSources(config)) return config; let removed = getIntermediateSources(config); let sources = config.sources.filter(x => !isSourceConfig(x) || !x.intermediate).map(cloneSource); for (let gap of removed) { let [gapStart, gapEnd] = gap.validWindow; let before = sources.find(x => isSourceConfig(x) && x.validWindow[1] === gapStart) as SourceConfig | undefined; let after = sources.find(x => isSourceConfig(x) && x.validWindow[0] === gapEnd) as SourceConfig | undefined; if (before && after && joinKey(before) === joinKey(after)) { before.validWindow = [before.validWindow[0], after.validWindow[1]]; sources.splice(sources.indexOf(after), 1); continue; } if (before) { before.validWindow = [before.validWindow[0], gapEnd]; continue; } if (after) { after.validWindow = [gapStart, after.validWindow[1]]; } } return { version: config.version, sources }; } /** Splits every source at splitUrl covering [start, end) so that middle window points at intermediateUrl instead, flagged as intermediate. Idempotent: a config that already contains the exact intermediate comes back unchanged. */ export function injectIntermediateSource(config: RemoteConfig, inject: { splitUrl: string; intermediateUrl: string; start: number; end: number }): RemoteConfig { let { splitUrl, intermediateUrl, start, end } = inject; if (start >= end) return config; if (config.sources.some(x => isSourceConfig(x) && x.intermediate && x.url === intermediateUrl && x.validWindow[0] === start && x.validWindow[1] === end)) { return config; } let sources: RemoteConfigBase[] = []; for (let source of config.sources) { if (!isSourceConfig(source) || source.url !== splitUrl || source.intermediate) { sources.push(cloneSource(source)); continue; } let [windowStart, windowEnd] = source.validWindow; if (windowEnd <= start || windowStart >= end) { sources.push(cloneSource(source)); continue; } let middleStart = Math.max(windowStart, start); let middleEnd = Math.min(windowEnd, end); if (middleStart > windowStart) { sources.push({ ...source, validWindow: [windowStart, middleStart] }); } // intermediate = the url this entry was split out of, so a request naming the intermediate still resolves to the original source sources.push({ ...source, url: intermediateUrl, validWindow: [middleStart, middleEnd], intermediate: splitUrl }); if (windowEnd > middleEnd) { sources.push({ ...source, validWindow: [middleEnd, windowEnd] }); } } return { version: config.version, sources }; } /** Intermediates whose window ended more than INTERMEDIATE_EXPIRE_GRACE ago are removed, and the windows they split are rejoined. */ export function expireIntermediateSources(config: RemoteConfig, now: number): RemoteConfig { let expired = getIntermediateSources(config).filter(x => x.validWindow[1] + INTERMEDIATE_EXPIRE_GRACE < now); if (!expired.length) return config; let keptIntermediates = getIntermediateSources(config).filter(x => !expired.includes(x)); let resolved = resolveIntermediateSources(config); let result = resolved; for (let keep of keptIntermediates) { result = injectIntermediateSource(result, { // The intermediate carries the url it was split out of; fall back to its own url only for a legacy entry that predates that splitUrl: keep.intermediate || keep.url, intermediateUrl: keep.url, start: keep.validWindow[0], end: keep.validWindow[1], }); } return result; }