1import type {
2 DesktopRuntimeInfo,
3 DesktopUpdateChannel,
4 DesktopUpdateReleaseNote,
5 DesktopUpdateState,
6} from "@t3tools/contracts";
7
8function nextStatusAfterDownloadFailure(
9 currentState: DesktopUpdateState,
10): DesktopUpdateState["status"] {
11 return currentState.availableVersion ? "available" : "error";
12}
13
14function getCanRetryAfterDownloadFailure(currentState: DesktopUpdateState): boolean {
15 return currentState.availableVersion !== null;
16}
17
18export function createInitialDesktopUpdateState(
19 currentVersion: string,
20 runtimeInfo: DesktopRuntimeInfo,
21 channel: DesktopUpdateChannel,
22): DesktopUpdateState {
23 return {
24 enabled: false,
25 status: "disabled",
26 channel,
27 currentVersion,
28 hostArch: runtimeInfo.hostArch,
29 appArch: runtimeInfo.appArch,
30 runningUnderArm64Translation: runtimeInfo.runningUnderArm64Translation,
31 availableVersion: null,
32 downloadedVersion: null,
33 releaseNotes: [],
34 omittedReleaseCount: 0,
35 downloadPercent: null,
36 checkedAt: null,
37 message: null,
38 errorContext: null,
39 canRetry: false,
40 };
41}
42
43export function reduceDesktopUpdateStateOnCheckStart(
44 state: DesktopUpdateState,
45 checkedAt: string,
46): DesktopUpdateState {
47 const hasDownloadedUpdate = state.downloadedVersion !== null;
48 return {
49 ...state,
50 status: "checking",
51 checkedAt,
52 releaseNotes: hasDownloadedUpdate ? state.releaseNotes : [],
53 omittedReleaseCount: hasDownloadedUpdate ? state.omittedReleaseCount : 0,
54 message: null,
55 downloadPercent: hasDownloadedUpdate ? 100 : null,
56 errorContext: null,
57 canRetry: false,
58 };
59}
60
61export function reduceDesktopUpdateStateOnCheckFailure(
62 state: DesktopUpdateState,
63 message: string,
64 checkedAt: string,
65): DesktopUpdateState {
66 if (state.downloadedVersion !== null) {
67 return {
68 ...state,
69 status: "downloaded",
70 message: null,
71 checkedAt,
72 downloadPercent: 100,
73 errorContext: null,
74 canRetry: true,
75 };
76 }
77
78 return {
79 ...state,
80 status: "error",
81 message,
82 checkedAt,
83 downloadPercent: null,
84 errorContext: "check",
85 canRetry: true,
86 };
87}
88
89export function reduceDesktopUpdateStateOnUpdateAvailable(
90 state: DesktopUpdateState,
91 version: string,
92 checkedAt: string,
93 releaseNotes: ReadonlyArray<DesktopUpdateReleaseNote> = [],
94 omittedReleaseCount = 0,
95): DesktopUpdateState {
96 const isDownloadedVersion = state.downloadedVersion === version;
97 const preserveReleaseNotes = isDownloadedVersion && releaseNotes.length === 0;
98 return {
99 ...state,
100 status: isDownloadedVersion ? "downloaded" : "available",
101 availableVersion: version,
102 installKind: isDownloadedVersion ? (state.installKind ?? "app") : "app",
103 downloadedVersion: isDownloadedVersion ? version : null,
104 releaseNotes: preserveReleaseNotes ? state.releaseNotes : releaseNotes,
105 omittedReleaseCount: preserveReleaseNotes ? state.omittedReleaseCount : omittedReleaseCount,
106 downloadPercent: isDownloadedVersion ? 100 : null,
107 checkedAt,
108 message: null,
109 errorContext: null,
110 canRetry: isDownloadedVersion,
111 };
112}
113
114export function reduceDesktopUpdateStateOnNoUpdate(
115 state: DesktopUpdateState,
116 checkedAt: string,
117): DesktopUpdateState {
118 if (state.downloadedVersion !== null) {
119 return {
120 ...state,
121 status: "downloaded",
122 availableVersion: state.downloadedVersion,
123 downloadPercent: 100,
124 checkedAt,
125 message: null,
126 errorContext: null,
127 canRetry: true,
128 };
129 }
130
131 return {
132 ...state,
133 status: "up-to-date",
134 availableVersion: null,
135 downloadedVersion: null,
136 releaseNotes: [],
137 omittedReleaseCount: 0,
138 downloadPercent: null,
139 checkedAt,
140 message: null,
141 errorContext: null,
142 canRetry: false,
143 };
144}
145
146export function reduceDesktopUpdateStateOnDownloadStart(
147 state: DesktopUpdateState,
148): DesktopUpdateState {
149 return {
150 ...state,
151 status: "downloading",
152 downloadPercent: 0,
153 message: null,
154 errorContext: null,
155 canRetry: false,
156 };
157}
158
159export function reduceDesktopUpdateStateOnDownloadFailure(
160 state: DesktopUpdateState,
161 message: string,
162): DesktopUpdateState {
163 return {
164 ...state,
165 status: nextStatusAfterDownloadFailure(state),
166 message,
167 downloadPercent: null,
168 errorContext: "download",
169 canRetry: getCanRetryAfterDownloadFailure(state),
170 };
171}
172
173export function reduceDesktopUpdateStateOnDownloadProgress(
174 state: DesktopUpdateState,
175 percent: number,
176): DesktopUpdateState {
177 return {
178 ...state,
179 status: "downloading",
180 downloadPercent: percent,
181 message: null,
182 errorContext: null,
183 canRetry: false,
184 };
185}
186
187export function reduceDesktopUpdateStateOnDownloadComplete(
188 state: DesktopUpdateState,
189 version: string,
190): DesktopUpdateState {
191 return {
192 ...state,
193 status: "downloaded",
194 availableVersion: version,
195 downloadedVersion: version,
196 downloadPercent: 100,
197 message: null,
198 errorContext: null,
199 canRetry: true,
200 };
201}
202
203export function reduceDesktopUpdateStateOnInstallFailure(
204 state: DesktopUpdateState,
205 message: string,
206): DesktopUpdateState {
207 return {
208 ...state,
209 status: "downloaded",
210 message,
211 errorContext: "install",
212 canRetry: true,
213 };
214}
215
216/** The install this app handed to the OS installer, saved right before it quits. */
217export interface DesktopUpdateInstallRecord {
218 readonly fromVersion: string;
219 readonly targetVersion: string;
220}
221
222/**
223 * Read on the next start: the version an install did not apply, because the
224 * app came back on the version it installed from. Null when the install
225 * applied, or when the app now runs some other version.
226 */
227export function resolveNotAppliedInstallVersion(
228 record: DesktopUpdateInstallRecord,
229 appVersion: string,
230): string | null {
231 return record.fromVersion === appVersion && record.targetVersion !== appVersion
232 ? record.targetVersion
233 : null;
234}