import type { DesktopUpdateStatus } from "../shared/update"; export type UpdateCheckTrigger = "boot" | "interval" | "manual"; export interface UpdateCheckPlan { readonly check: boolean; readonly promptVersionAfterCheck: string | null;} export interface CompletedUpdateCheckInput { readonly stagedVersion: string | null; readonly trigger: UpdateCheckTrigger; readonly updateAvailable: boolean; readonly availableVersion: string | null;} export interface DownloadedUpdateInput { readonly stagedVersion: string | null; readonly declinedVersion: string | null; readonly incomingVersion: string; readonly trigger: UpdateCheckTrigger;} export interface DownloadedUpdatePlan { readonly stagedVersion: string; readonly declinedVersion: string | null; readonly status: DesktopUpdateStatus; readonly promptVersion: string | null;} const parseVersionParts = (version: string): readonly number[] | null => { const core = version.trim().split(/[+-]/, 1)[0]; if (!core) return null; const parts = core.split(".").map((part) => Number.parseInt(part, 10)); return parts.every((part) => Number.isInteger(part) && part >= 0) ? parts : null;}; export const compareUpdateVersions = (left: string, right: string): number | null => { const leftParts = parseVersionParts(left); const rightParts = parseVersionParts(right); if (!leftParts || !rightParts) return null; const length = Math.max(leftParts.length, rightParts.length); for (let index = 0; index < length; index += 1) { const l = leftParts[index] ?? 0; const r = rightParts[index] ?? 0; if (l !== r) return l > r ? 1 : -1; } return 0;}; const isNewerUpdateVersion = (incomingVersion: string, declinedVersion: string): boolean => { const comparison = compareUpdateVersions(incomingVersion, declinedVersion); return comparison === null ? incomingVersion !== declinedVersion : comparison > 0;}; export const planUpdateCheck = (input: { readonly stagedVersion: string | null; readonly trigger: UpdateCheckTrigger;}): UpdateCheckPlan => ({ check: true, promptVersionAfterCheck: input.trigger === "manual" ? input.stagedVersion : null,}); export const planCompletedUpdateCheck = ( input: CompletedUpdateCheckInput,): { readonly promptVersion: string | null } => { if (input.trigger !== "manual" || !input.stagedVersion) return { promptVersion: null }; if (!input.updateAvailable) return { promptVersion: input.stagedVersion }; if (!input.availableVersion) return { promptVersion: null }; const comparison = compareUpdateVersions(input.availableVersion, input.stagedVersion); if (comparison === null) { return { promptVersion: input.availableVersion === input.stagedVersion ? input.stagedVersion : null, }; } return { promptVersion: comparison <= 0 ? input.stagedVersion : null };}; export const planDownloadedUpdate = (input: DownloadedUpdateInput): DownloadedUpdatePlan => { const prompt = input.trigger === "manual" || !input.declinedVersion || isNewerUpdateVersion(input.incomingVersion, input.declinedVersion); return { stagedVersion: input.incomingVersion, declinedVersion: input.declinedVersion, status: { state: "downloaded", version: input.incomingVersion }, promptVersion: prompt ? input.incomingVersion : null, };}; export const statusAfterUpdateError = ( status: DesktopUpdateStatus, message: string,): DesktopUpdateStatus => { if (status.state === "available" || status.state === "downloading" || status.state === "error") { return { state: "error", version: status.version, message }; } return status;}; export const planFatalAutoInstallOnQuit = (input: { readonly packaged: boolean; readonly retryAfterReset: boolean;}): { readonly enableDuringFailure: boolean; readonly restoreAfterRecovery: boolean } => ({ enableDuringFailure: input.packaged, restoreAfterRecovery: input.packaged && input.retryAfterReset,});
import type { DesktopUpdateStatus } from "../shared/update";
export type UpdateCheckTrigger = "boot" | "interval" | "manual";
export interface UpdateCheckPlan {
readonly check: boolean;
readonly promptVersionAfterCheck: string | null;
}
export interface CompletedUpdateCheckInput {
readonly stagedVersion: string | null;
readonly trigger: UpdateCheckTrigger;
readonly updateAvailable: boolean;
readonly availableVersion: string | null;
}
export interface DownloadedUpdateInput {
readonly stagedVersion: string | null;
readonly declinedVersion: string | null;
readonly incomingVersion: string;
readonly trigger: UpdateCheckTrigger;
}
export interface DownloadedUpdatePlan {
readonly stagedVersion: string;
readonly declinedVersion: string | null;
readonly status: DesktopUpdateStatus;
readonly promptVersion: string | null;
}
const parseVersionParts = (version: string): readonly number[] | null => {
const core = version.trim().split(/[+-]/, 1)[0];
if (!core) return null;
const parts = core.split(".").map((part) => Number.parseInt(part, 10));
return parts.every((part) => Number.isInteger(part) && part >= 0) ? parts : null;
};
export const compareUpdateVersions = (left: string, right: string): number | null => {
const leftParts = parseVersionParts(left);
const rightParts = parseVersionParts(right);
if (!leftParts || !rightParts) return null;
const length = Math.max(leftParts.length, rightParts.length);
for (let index = 0; index < length; index += 1) {
const l = leftParts[index] ?? 0;
const r = rightParts[index] ?? 0;
if (l !== r) return l > r ? 1 : -1;
}
return 0;
};
const isNewerUpdateVersion = (incomingVersion: string, declinedVersion: string): boolean => {
const comparison = compareUpdateVersions(incomingVersion, declinedVersion);
return comparison === null ? incomingVersion !== declinedVersion : comparison > 0;
};
export const planUpdateCheck = (input: {
readonly stagedVersion: string | null;
readonly trigger: UpdateCheckTrigger;
}): UpdateCheckPlan => ({
check: true,
promptVersionAfterCheck: input.trigger === "manual" ? input.stagedVersion : null,
});
export const planCompletedUpdateCheck = (
input: CompletedUpdateCheckInput,
): { readonly promptVersion: string | null } => {
if (input.trigger !== "manual" || !input.stagedVersion) return { promptVersion: null };
if (!input.updateAvailable) return { promptVersion: input.stagedVersion };
if (!input.availableVersion) return { promptVersion: null };
const comparison = compareUpdateVersions(input.availableVersion, input.stagedVersion);
if (comparison === null) {
return {
promptVersion: input.availableVersion === input.stagedVersion ? input.stagedVersion : null,
};
}
return { promptVersion: comparison <= 0 ? input.stagedVersion : null };
};
export const planDownloadedUpdate = (input: DownloadedUpdateInput): DownloadedUpdatePlan => {
const prompt =
input.trigger === "manual" ||
!input.declinedVersion ||
isNewerUpdateVersion(input.incomingVersion, input.declinedVersion);
return {
stagedVersion: input.incomingVersion,
declinedVersion: input.declinedVersion,
status: { state: "downloaded", version: input.incomingVersion },
promptVersion: prompt ? input.incomingVersion : null,
};
};
export const statusAfterUpdateError = (
status: DesktopUpdateStatus,
message: string,
): DesktopUpdateStatus => {
if (status.state === "available" || status.state === "downloading" || status.state === "error") {
return { state: "error", version: status.version, message };
}
return status;
};
export const planFatalAutoInstallOnQuit = (input: {
readonly packaged: boolean;
readonly retryAfterReset: boolean;
}): { readonly enableDuringFailure: boolean; readonly restoreAfterRecovery: boolean } => ({
enableDuringFailure: input.packaged,
restoreAfterRecovery: input.packaged && input.retryAfterReset,
});