Files
orca/src/shared/app-version.ts
Neil e1071f59e9 Say why an update install failed instead of stalling for three minutes (#12224)
Surfaces the real install-failure cause instead of letting a failed elevation stall silently, and keeps the reconnect wait inside its total budget by recomputing the remaining time after each awaited RPC.

Relates to #11906 — this fixes the observability half. The functional half (a .deb/.rpm host cannot elevate and can never self-update) is unchanged, so the issue stays open.
2026-08-04 02:02:53 -07:00

107 lines
2.8 KiB
TypeScript

type ParsedVersion = {
core: [number, number, number]
prerelease: string[]
}
function parseVersion(value: string): ParsedVersion | null {
const normalized = value.trim().replace(/^v/i, '')
const match = normalized.match(
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-.]+))?$/
)
if (!match) {
return null
}
return {
core: [Number(match[1]), Number(match[2]), Number(match[3])],
prerelease: match[4]?.split('.') ?? []
}
}
export function isValidAppVersion(value: string): boolean {
return parseVersion(value) !== null
}
export function isPrereleaseAppVersion(value: string): boolean {
const parsed = parseVersion(value)
return parsed !== null && parsed.prerelease.length > 0
}
export function isPerfPrereleaseAppVersion(value: string): boolean {
const parsed = parseVersion(value)
return parsed?.prerelease.some((identifier) => identifier.toLowerCase() === 'perf') ?? false
}
function compareIdentifiers(left: string, right: string): number {
const leftNumeric = /^\d+$/.test(left)
const rightNumeric = /^\d+$/.test(right)
if (leftNumeric && rightNumeric) {
return Number(left) - Number(right)
}
if (leftNumeric) {
return -1
}
if (rightNumeric) {
return 1
}
return left.localeCompare(right)
}
/** Returns negative if left < right, 0 if equal, positive if left > right. */
export function compareAppVersions(left: string, right: string): number {
const leftVersion = parseVersion(left)
const rightVersion = parseVersion(right)
if (!leftVersion || !rightVersion) {
return 0
}
for (let index = 0; index < leftVersion.core.length; index += 1) {
const leftPart = leftVersion.core[index]
const rightPart = rightVersion.core[index]
if (leftPart !== rightPart) {
return leftPart - rightPart
}
}
const leftPrerelease = leftVersion.prerelease
const rightPrerelease = rightVersion.prerelease
if (leftPrerelease.length === 0 && rightPrerelease.length === 0) {
return 0
}
if (leftPrerelease.length === 0) {
return 1
}
if (rightPrerelease.length === 0) {
return -1
}
for (let index = 0; index < Math.max(leftPrerelease.length, rightPrerelease.length); index += 1) {
const leftPart = leftPrerelease[index]
const rightPart = rightPrerelease[index]
if (leftPart === undefined) {
return -1
}
if (rightPart === undefined) {
return 1
}
const comparison = compareIdentifiers(leftPart, rightPart)
if (comparison !== 0) {
return comparison
}
}
return 0
}
/** True when `current` is at or past `target`. Unparseable or absent versions never qualify. */
export function hasReachedAppVersion(current: string, target: string | null): boolean {
return (
target !== null &&
isValidAppVersion(current) &&
isValidAppVersion(target) &&
compareAppVersions(current, target) >= 0
)
}