Skip to content

Commit b44af55

Browse files
committed
fix: keep ElectronVersions' version cache current.
The existing public API is synchronous (which makes sense given that you only get new versions once or twice a day) and we don't want to block on http fetch calls, so updates happen in the background and are kicked off by a freshness check after every public API call.
1 parent 7480f56 commit b44af55

File tree

1 file changed

+100
-16
lines changed

1 file changed

+100
-16
lines changed

src/versions.ts

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,9 @@ const NUM_SUPPORTED_MAJORS = 4;
8888
export class BaseVersions implements Versions {
8989
private readonly map = new Map<string, SemVer>();
9090

91-
public constructor(val: unknown) {
91+
protected setVersions(val: unknown) {
9292
// build the array
9393
let parsed: Array<SemVer | null> = [];
94-
9594
if (isArrayOfVersionObjects(val)) {
9695
parsed = val.map(({ version }) => semverParse(version));
9796
} else if (isArrayOfStrings(val)) {
@@ -101,7 +100,12 @@ export class BaseVersions implements Versions {
101100
// insert them in sorted order
102101
const semvers = parsed.filter((sem) => Boolean(sem)) as SemVer[];
103102
semvers.sort((a, b) => compareVersions(a, b));
104-
this.map = new Map(semvers.map((sem) => [sem.version, sem]));
103+
this.map.clear();
104+
for (const sem of semvers) this.map.set(sem.version, sem);
105+
}
106+
107+
public constructor(versions: unknown) {
108+
this.setVersions(versions);
105109
}
106110

107111
public get prereleaseMajors(): number[] {
@@ -186,32 +190,112 @@ export class BaseVersions implements Versions {
186190
* This is generally what to use in production.
187191
*/
188192
export class ElectronVersions extends BaseVersions {
189-
private constructor(values: unknown) {
193+
private static readonly freshnessMs = 4 * 60 * 60 * 1000; // cache for N hours
194+
195+
private constructor(
196+
private readonly versionsCache: string,
197+
private mtimeMs: number,
198+
values: unknown,
199+
) {
190200
super(values);
191201
}
192202

203+
private static async fetchVersions(cacheFile: string): Promise<unknown> {
204+
const d = debug('fiddle-core:ElectronVersions:fetchVersions');
205+
const url = 'https://releases.electronjs.org/releases.json';
206+
d('fetching releases list from', url);
207+
const response = await fetch(url);
208+
const json = (await response.json()) as unknown;
209+
await fs.outputJson(cacheFile, json);
210+
return json;
211+
}
212+
193213
public static async create(
194214
paths: Partial<Paths> = {},
195215
): Promise<ElectronVersions> {
196216
const d = debug('fiddle-core:ElectronVersions:create');
197217
const { versionsCache } = { ...DefaultPaths, ...paths };
218+
219+
let versions: unknown;
220+
const now = Date.now();
198221
try {
199222
const st = await fs.stat(versionsCache);
200-
const cacheIntervalMs = 4 * 60 * 60 * 1000; // re-fetch after 4 hours
201-
if (st.mtime.getTime() + cacheIntervalMs > Date.now()) {
202-
return new ElectronVersions(await fs.readJson(versionsCache));
223+
if (st.mtimeMs + ElectronVersions.freshnessMs >= now)
224+
versions = (await fs.readJson(versionsCache)) as unknown;
225+
} catch (err) {
226+
// cache file missing
227+
}
228+
229+
if (!versions) {
230+
try {
231+
versions = await ElectronVersions.fetchVersions(versionsCache);
232+
} catch (err) {
233+
d('error fetching versions', err);
203234
}
235+
}
236+
237+
return new ElectronVersions(versionsCache, now, versions);
238+
}
239+
240+
// upate the cache if it's too old
241+
private async keepFresh(): Promise<void> {
242+
const d = debug('fiddle-core:ElectronVersions:keepFresh');
243+
244+
// if it's still fresh, do nothing
245+
const { mtimeMs, versionsCache } = this;
246+
const now = Date.now();
247+
if (mtimeMs + ElectronVersions.freshnessMs >= now) return;
248+
249+
// update the cache
250+
try {
251+
this.mtimeMs = now;
252+
const versions = await ElectronVersions.fetchVersions(versionsCache);
253+
this.setVersions(versions);
254+
d(`saved "${versionsCache}"`);
204255
} catch (err) {
205-
// if no cache, fetch from electronjs.org
206-
d(`unable to stat cache file "${versionsCache}"`, err);
256+
d('error fetching versions', err);
257+
this.mtimeMs = mtimeMs;
207258
}
259+
}
208260

209-
const url = 'https://releases.electronjs.org/releases.json';
210-
d('fetching releases list from', url);
211-
const response = await fetch(url);
212-
const json = (await response.json()) as unknown;
213-
await fs.outputJson(versionsCache, json);
214-
d(`saved "${versionsCache}"`);
215-
return new ElectronVersions(json);
261+
public override get prereleaseMajors(): number[] {
262+
void this.keepFresh();
263+
return super.prereleaseMajors;
264+
}
265+
public override get stableMajors(): number[] {
266+
void this.keepFresh();
267+
return super.stableMajors;
268+
}
269+
public override get supportedMajors(): number[] {
270+
void this.keepFresh();
271+
return super.supportedMajors;
272+
}
273+
public override get obsoleteMajors(): number[] {
274+
void this.keepFresh();
275+
return super.obsoleteMajors;
276+
}
277+
public override get versions(): SemVer[] {
278+
void this.keepFresh();
279+
return super.versions;
280+
}
281+
public override get latest(): SemVer | undefined {
282+
void this.keepFresh();
283+
return super.latest;
284+
}
285+
public override get latestStable(): SemVer | undefined {
286+
void this.keepFresh();
287+
return super.latestStable;
288+
}
289+
public override isVersion(ver: SemOrStr): boolean {
290+
void this.keepFresh();
291+
return super.isVersion(ver);
292+
}
293+
public override inMajor(major: number): SemVer[] {
294+
void this.keepFresh();
295+
return super.inMajor(major);
296+
}
297+
public override inRange(a: SemOrStr, b: SemOrStr): SemVer[] {
298+
void this.keepFresh();
299+
return super.inRange(a, b);
216300
}
217301
}

0 commit comments

Comments
 (0)