Skip to content

Commit 0080d68

Browse files
Add caching
1 parent c9b61aa commit 0080d68

File tree

4 files changed

+149
-20
lines changed

4 files changed

+149
-20
lines changed

packages/snaps-utils/src/fs.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { promises as fs } from 'fs';
22
import * as path from 'path';
3-
import { join } from 'path';
3+
import { join, dirname } from 'path';
44

55
import {
66
getOutfilePath,
77
isDirectory,
88
isFile,
99
readJsonFile,
10+
useFileSystemCache,
1011
useTemporaryFile,
1112
validateDirPath,
1213
validateFilePath,
@@ -20,13 +21,15 @@ jest.mock('fs');
2021

2122
const BASE_PATH = '/snap';
2223
const MANIFEST_PATH = join(BASE_PATH, NpmSnapFileNames.Manifest);
24+
const CACHE_PATH = join(process.cwd(), 'node_modules/.cache');
2325

2426
/**
2527
* Clears out all the files in the in-memory file system, and writes the default
2628
* files to the `BASE_PATH` folder, including sub-folders.
2729
*/
2830
async function resetFileSystem() {
2931
await fs.rm(BASE_PATH, { recursive: true, force: true });
32+
await fs.rm(CACHE_PATH, { recursive: true, force: true });
3033

3134
// Create `dist` folder.
3235
await fs.mkdir(join(BASE_PATH, 'dist'), { recursive: true });
@@ -262,3 +265,73 @@ describe('useTemporaryFile', () => {
262265
expect(await isFile(filePath)).toBe(false);
263266
});
264267
});
268+
269+
describe('useFileSystemCache', () => {
270+
beforeEach(async () => {
271+
await resetFileSystem();
272+
});
273+
274+
const cachedFunction = useFileSystemCache('foo', 5000, async () => {
275+
return 'foo';
276+
});
277+
278+
const cachedFilePath = join(CACHE_PATH, 'foo.json');
279+
280+
it('writes cached value to the file system', async () => {
281+
const spy = jest.spyOn(fs, 'writeFile');
282+
expect(await cachedFunction()).toBe('foo');
283+
284+
expect(spy).toHaveBeenCalledTimes(1);
285+
286+
const cacheValue = await fs.readFile(cachedFilePath, 'utf8');
287+
const cacheJson = JSON.parse(cacheValue);
288+
289+
expect(cacheJson).toStrictEqual({
290+
timestamp: expect.any(Number),
291+
value: 'foo',
292+
});
293+
});
294+
295+
it('reads cached value from the file system', async () => {
296+
const readSpy = jest.spyOn(fs, 'readFile');
297+
const writeSpy = jest.spyOn(fs, 'writeFile');
298+
299+
expect(await cachedFunction()).toBe('foo');
300+
expect(await cachedFunction()).toBe('foo');
301+
302+
expect(readSpy).toHaveBeenCalledTimes(2);
303+
expect(writeSpy).toHaveBeenCalledTimes(1);
304+
305+
const cacheValue = await fs.readFile(cachedFilePath, 'utf8');
306+
const cacheJson = JSON.parse(cacheValue);
307+
308+
expect(cacheJson).toStrictEqual({
309+
timestamp: expect.any(Number),
310+
value: 'foo',
311+
});
312+
});
313+
314+
it('discards cached value if it is expired', async () => {
315+
await fs.mkdir(dirname(cachedFilePath), { recursive: true });
316+
await fs.writeFile(
317+
cachedFilePath,
318+
JSON.stringify({ timestamp: Date.now() - 6000, value: 'bar' }),
319+
);
320+
321+
const readSpy = jest.spyOn(fs, 'readFile');
322+
const writeSpy = jest.spyOn(fs, 'writeFile');
323+
324+
expect(await cachedFunction()).toBe('foo');
325+
326+
expect(readSpy).toHaveBeenCalledTimes(1);
327+
expect(writeSpy).toHaveBeenCalledTimes(1);
328+
329+
const cacheValue = await fs.readFile(cachedFilePath, 'utf8');
330+
const cacheJson = JSON.parse(cacheValue);
331+
332+
expect(cacheJson).toStrictEqual({
333+
timestamp: expect.any(Number),
334+
value: 'foo',
335+
});
336+
});
337+
});

packages/snaps-utils/src/fs.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,51 @@ export async function useTemporaryFile<Type = unknown>(
182182
}
183183
}
184184
}
185+
186+
/**
187+
* Use the file system to cache a return value with a given key and TTL.
188+
*
189+
* @param cacheKey - The key to use for the cache.
190+
* @param ttl - The time-to-live.
191+
* @param fn - The callback function to wrap.
192+
* @returns The result from the callback.
193+
*/
194+
export function useFileSystemCache<Type = unknown>(
195+
cacheKey: string,
196+
ttl: number,
197+
fn: () => Promise<Type>,
198+
) {
199+
return async () => {
200+
const filePath = pathUtils.join(
201+
process.cwd(),
202+
'node_modules/.cache',
203+
`${cacheKey}.json`,
204+
);
205+
206+
try {
207+
const cacheContents = await fs.readFile(filePath, 'utf8');
208+
const json = JSON.parse(cacheContents);
209+
210+
if (json.timestamp + ttl > Date.now()) {
211+
return json.value;
212+
}
213+
} catch {
214+
// No-op
215+
}
216+
217+
const value = await fn();
218+
219+
try {
220+
await fs.mkdir(pathUtils.dirname(filePath), { recursive: true });
221+
222+
const json = { timestamp: Date.now(), value };
223+
await fs.writeFile(filePath, JSON.stringify(json), {
224+
encoding: 'utf8',
225+
});
226+
} catch {
227+
// No-op
228+
}
229+
230+
return value;
231+
};
232+
}

packages/snaps-utils/src/manifest/validators/production-platform-version.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import fetchMock from 'jest-fetch-mock';
55
import { productionPlatformVersion } from './production-platform-version';
66
import { getMockSnapFiles, getSnapManifest } from '../../test-utils';
77

8+
jest.mock('fs');
9+
810
const MOCK_GITHUB_RESPONSE = JSON.stringify({
911
// eslint-disable-next-line @typescript-eslint/naming-convention
1012
target_commitish: '5fceb7ed2ef18a3984786db1161a76ca5c8e15b9',

packages/snaps-utils/src/manifest/validators/production-platform-version.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { Duration, inMilliseconds } from '@metamask/utils';
12
import { minVersion, gt } from 'semver';
23

4+
import { useFileSystemCache } from '../../fs';
35
import type { ValidatorMeta } from '../validator-types';
46

57
/**
@@ -8,30 +10,34 @@ import type { ValidatorMeta } from '../validator-types';
810
*
911
* @returns The production version of the Snaps platform or null if any error occurred.
1012
*/
11-
async function determineProductionVersion() {
12-
try {
13-
// TODO: Cache this check.
14-
const latestRelease = await fetch(
15-
'https://api.github.com/repos/metamask/metamask-extension/releases/latest',
16-
);
13+
const determineProductionVersion = useFileSystemCache(
14+
'snaps-production-version',
15+
inMilliseconds(7, Duration.Day),
16+
async () => {
17+
try {
18+
// TODO: Cache this check.
19+
const latestRelease = await fetch(
20+
'https://api.github.com/repos/metamask/metamask-extension/releases/latest',
21+
);
1722

18-
const latestReleaseJson = await latestRelease.json();
23+
const latestReleaseJson = await latestRelease.json();
1924

20-
const latestReleaseCommit = latestReleaseJson.target_commitish;
25+
const latestReleaseCommit = latestReleaseJson.target_commitish;
2126

22-
const packageJsonResponse = await fetch(
23-
`https://raw.githubusercontent.com/MetaMask/metamask-extension/${latestReleaseCommit}/package.json`,
24-
);
27+
const packageJsonResponse = await fetch(
28+
`https://raw.githubusercontent.com/MetaMask/metamask-extension/${latestReleaseCommit}/package.json`,
29+
);
2530

26-
const packageJson = await packageJsonResponse.json();
31+
const packageJson = await packageJsonResponse.json();
2732

28-
const versionRange = packageJson.dependencies['@metamask/snaps-sdk'];
33+
const versionRange = packageJson.dependencies['@metamask/snaps-sdk'];
2934

30-
return minVersion(versionRange);
31-
} catch {
32-
return null;
33-
}
34-
}
35+
return minVersion(versionRange)?.format();
36+
} catch {
37+
return null;
38+
}
39+
},
40+
);
3541

3642
/**
3743
* Check if the platform version in manifest exceeds the version
@@ -54,7 +60,7 @@ export const productionPlatformVersion: ValidatorMeta = {
5460

5561
if (gt(manifestPlatformVersion, maximumVersion)) {
5662
context.report(
57-
`The "platformVersion" in use is not supported in the production version of MetaMask yet. The current production version is "${maximumVersion.format()}".`,
63+
`The "platformVersion" in use is not supported in the production version of MetaMask yet. The current production version is "${maximumVersion}".`,
5864
);
5965
}
6066
},

0 commit comments

Comments
 (0)