-
Notifications
You must be signed in to change notification settings - Fork 58
/
update-notification-manager.ts
134 lines (119 loc) · 4.06 KB
/
update-notification-manager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import semver from 'semver';
import { promises as fs } from 'fs';
import importNodeFetch from '@mongosh/import-node-fetch';
import type {
RequestInfo,
RequestInit,
Response,
} from '@mongosh/import-node-fetch';
// 'http' is not supported in startup snapshots yet.
const fetch = async (url: RequestInfo, init?: RequestInit): Promise<Response> =>
await (await importNodeFetch()).default(url, init);
interface MongoshUpdateLocalFileContents {
lastChecked?: number;
latestKnownMongoshVersion?: string;
etag?: string;
updateURL?: string;
}
// Utility for fetching metadata about potentially available newer versions
// and returning that latest version if available.
export class UpdateNotificationManager {
private latestKnownMongoshVersion: string | undefined = undefined;
private localFilesystemFetchInProgress: Promise<unknown> | undefined =
undefined;
async getLatestVersionIfMoreRecent(
currentVersion: string
): Promise<string | null> {
try {
await this.localFilesystemFetchInProgress;
} catch {
/* already handled in fetchUpdateMetadata() */
}
if (!this.latestKnownMongoshVersion) return null;
if (
currentVersion &&
!semver.gt(this.latestKnownMongoshVersion, currentVersion)
)
return null;
if (currentVersion && semver.prerelease(currentVersion)) return null;
return this.latestKnownMongoshVersion;
}
// Fetch update metadata, taking into account a local cache and an external
// JSON feed. This function will throw in case it failed to load information
// about latest versions.
async fetchUpdateMetadata(
updateURL: string,
localFilePath: string
): Promise<void> {
let localFileContents: MongoshUpdateLocalFileContents | undefined;
await (this.localFilesystemFetchInProgress = (async () => {
let localFileText = '';
try {
localFileText = await fs.readFile(localFilePath, 'utf-8');
} catch (err: unknown) {
// Do not fail if the error is just ENOENT
if (
!(
err &&
typeof err === 'object' &&
'code' in err &&
err.code === 'ENOENT'
)
)
throw err;
}
try {
localFileContents = JSON.parse(localFileText);
} catch {
// ignore possibly corrupted file contents
}
if (localFileContents?.updateURL !== updateURL) {
// Invalidate local cache if the source URL has changed.
localFileContents = undefined;
}
if (localFileContents?.latestKnownMongoshVersion) {
this.latestKnownMongoshVersion =
localFileContents.latestKnownMongoshVersion;
}
this.localFilesystemFetchInProgress = undefined;
})());
if (
localFileContents?.lastChecked &&
Date.now() - localFileContents.lastChecked < 86400_000
) {
return;
}
const response = await fetch(updateURL, {
headers: localFileContents?.etag
? { 'if-none-match': localFileContents?.etag }
: {},
});
if (response.status === 304 /* Not Modified, i.e. ETag matched */) {
response.body
?.once('error', () => {
/* ignore response content and errors */
})
.resume();
localFileContents = { ...localFileContents, lastChecked: Date.now() };
await fs.writeFile(localFilePath, JSON.stringify(localFileContents));
return;
}
if (!response.ok || !response.body) {
throw new Error(
`Unexpected status code fetching ${updateURL}: ${response.status} ${response.statusText}`
);
}
const jsonContents = (await response.json()) as { versions?: any[] };
this.latestKnownMongoshVersion = jsonContents?.versions
?.map((v: any) => v.version as string)
?.filter((v) => !semver.prerelease(v))
?.sort(semver.rcompare)?.[0];
localFileContents = {
updateURL,
lastChecked: Date.now(),
etag: response.headers.get('etag') ?? undefined,
latestKnownMongoshVersion: this.latestKnownMongoshVersion,
};
await fs.writeFile(localFilePath, JSON.stringify(localFileContents));
}
}