-
Notifications
You must be signed in to change notification settings - Fork 542
/
http.ts
111 lines (94 loc) · 3.12 KB
/
http.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
import {
SnapManifest,
assertIsSnapManifest,
VirtualFile,
HttpSnapIdStruct,
NpmSnapFileNames,
} from '@metamask/snaps-utils';
import { assert, assertStruct } from '@metamask/utils';
import { ensureRelative } from '../../utils';
import { SnapLocation } from './location';
export interface HttpOptions {
/**
* @default fetch
*/
fetch?: typeof fetch;
fetchOptions?: RequestInit;
}
export class HttpLocation implements SnapLocation {
// We keep contents separate because then we can use only one Blob in cache,
// which we convert to Uint8Array when actually returning the file.
//
// That avoids deepCloning file contents.
// I imagine ArrayBuffers are copy-on-write optimized, meaning
// in most often case we'll only have one file contents in common case.
private readonly cache = new Map<
string,
{ file: VirtualFile; contents: Blob }
>();
private validatedManifest?: VirtualFile<SnapManifest>;
private readonly url: URL;
private readonly fetchFn: typeof fetch;
private readonly fetchOptions?: RequestInit;
constructor(url: URL, opts: HttpOptions = {}) {
assertStruct(url.toString(), HttpSnapIdStruct, 'Invalid Snap Id: ');
this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
this.fetchOptions = opts.fetchOptions;
this.url = url;
}
async manifest(): Promise<VirtualFile<SnapManifest>> {
if (this.validatedManifest) {
return this.validatedManifest.clone();
}
// jest-fetch-mock doesn't handle new URL(), we need to convert .toString()
const canonicalPath = new URL(
NpmSnapFileNames.Manifest,
this.url,
).toString();
const contents = await (
await this.fetchFn(canonicalPath, this.fetchOptions)
).text();
const manifest = JSON.parse(contents);
assertIsSnapManifest(manifest);
const vfile = new VirtualFile<SnapManifest>({
value: contents,
result: manifest,
path: `./${NpmSnapFileNames.Manifest}`,
data: { canonicalPath },
});
this.validatedManifest = vfile;
return this.manifest();
}
async fetch(path: string): Promise<VirtualFile> {
const relativePath = ensureRelative(path);
const cached = this.cache.get(relativePath);
if (cached !== undefined) {
const { file, contents } = cached;
const value = new Uint8Array(await contents.arrayBuffer());
const vfile = file.clone();
vfile.value = value;
return vfile;
}
const canonicalPath = this.toCanonical(relativePath).toString();
const response = await this.fetchFn(canonicalPath, this.fetchOptions);
const vfile = new VirtualFile({
value: '',
path: relativePath,
data: { canonicalPath },
});
const blob = await response.blob();
assert(
!this.cache.has(relativePath),
'Corrupted cache, multiple files with same path.',
);
this.cache.set(relativePath, { file: vfile, contents: blob });
return this.fetch(relativePath);
}
get root(): URL {
return new URL(this.url);
}
private toCanonical(path: string): URL {
assert(!path.startsWith('/'), 'Tried to parse absolute path.');
return new URL(path, this.url);
}
}