-
Notifications
You must be signed in to change notification settings - Fork 736
/
index.ts
177 lines (155 loc) · 5.29 KB
/
index.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import { WorkerEntrypoint } from "cloudflare:workers";
import { PerformanceTimer } from "../../utils/performance";
import { setupSentry } from "../../utils/sentry";
import { Analytics } from "./analytics";
import { AssetsManifest } from "./assets-manifest";
import { applyConfigurationDefaults } from "./configuration";
import { decodePath, getIntent, handleRequest } from "./handler";
import {
InternalServerErrorResponse,
MethodNotAllowedResponse,
} from "./responses";
import { getAssetWithMetadataFromKV } from "./utils/kv";
import type { AssetConfig, UnsafePerformanceTimer } from "../../utils/types";
import type { ColoMetadata, Environment, ReadyAnalytics } from "./types";
type Env = {
/*
* ASSETS_MANIFEST is a pipeline binding to an ArrayBuffer containing the
* binary-encoded site manifest
*/
ASSETS_MANIFEST: ArrayBuffer;
/*
* ASSETS_KV_NAMESPACE is a pipeline binding to the KV namespace that the
* assets are in.
*/
ASSETS_KV_NAMESPACE: KVNamespace;
CONFIG: AssetConfig;
SENTRY_DSN: string;
SENTRY_ACCESS_CLIENT_ID: string;
SENTRY_ACCESS_CLIENT_SECRET: string;
ENVIRONMENT: Environment;
ANALYTICS: ReadyAnalytics;
COLO_METADATA: ColoMetadata;
UNSAFE_PERFORMANCE: UnsafePerformanceTimer;
VERSION_METADATA: WorkerVersionMetadata;
};
/*
* The Asset Worker is currently set up as a `WorkerEntrypoint` class so
* that it is able to accept RPC calls to any of its public methods. There
* are currently four such public methods defined on this Worker:
* `canFetch`, `getByETag`, `getByPathname` and `exists`. While we are
* stabilising the implementation details of these methods, we would like
* to prevent developers from having their Workers call these methods
* directly. To that end, we are adopting the `unstable_<method_name>`
* naming convention for all of the aforementioned methods, to indicate that
* they are still in flux and that they are not an established API contract.
*/
export default class extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
let sentry: ReturnType<typeof setupSentry> | undefined;
const analytics = new Analytics(this.env.ANALYTICS);
const performance = new PerformanceTimer(this.env.UNSAFE_PERFORMANCE);
const startTimeMs = performance.now();
try {
sentry = setupSentry(
request,
this.ctx,
this.env.SENTRY_DSN,
this.env.SENTRY_ACCESS_CLIENT_ID,
this.env.SENTRY_ACCESS_CLIENT_SECRET
);
const config = applyConfigurationDefaults(this.env.CONFIG);
const userAgent = request.headers.get("user-agent") ?? "UA UNKNOWN";
if (sentry) {
const colo = this.env.COLO_METADATA.coloId;
sentry.setTag("colo", this.env.COLO_METADATA.coloId);
sentry.setTag("metal", this.env.COLO_METADATA.metalId);
sentry.setUser({ userAgent: userAgent, colo: colo });
}
if (this.env.COLO_METADATA && this.env.VERSION_METADATA) {
const url = new URL(request.url);
analytics.setData({
coloId: this.env.COLO_METADATA.coloId,
metalId: this.env.COLO_METADATA.metalId,
coloTier: this.env.COLO_METADATA.coloTier,
coloRegion: this.env.COLO_METADATA.coloRegion,
version: this.env.VERSION_METADATA.id,
hostname: url.hostname,
htmlHandling: config.html_handling,
notFoundHandling: config.not_found_handling,
userAgent: userAgent,
});
}
return handleRequest(
request,
config,
this.unstable_exists.bind(this),
this.unstable_getByETag.bind(this)
);
} catch (err) {
const response = new InternalServerErrorResponse(err as Error);
// Log to Sentry if we can
if (sentry) {
sentry.captureException(err);
}
if (err instanceof Error) {
analytics.setData({ error: err.message });
}
return response;
} finally {
analytics.setData({ requestTime: performance.now() - startTimeMs });
analytics.write();
}
}
async unstable_canFetch(request: Request): Promise<boolean | Response> {
const url = new URL(request.url);
const method = request.method.toUpperCase();
const decodedPathname = decodePath(url.pathname);
const intent = await getIntent(
decodedPathname,
{
...applyConfigurationDefaults(this.env.CONFIG),
not_found_handling: "none",
},
this.unstable_exists.bind(this)
);
// if asset exists but non GET/HEAD method, 405
if (intent && ["GET", "HEAD"].includes(method)) {
return new MethodNotAllowedResponse();
}
if (intent === null) {
return false;
}
return true;
}
async unstable_getByETag(
eTag: string
): Promise<{ readableStream: ReadableStream; contentType: string }> {
const asset = await getAssetWithMetadataFromKV(
this.env.ASSETS_KV_NAMESPACE,
eTag
);
if (!asset || !asset.value) {
throw new Error(
`Requested asset ${eTag} exists in the asset manifest but not in the KV namespace.`
);
}
return {
readableStream: asset.value,
contentType: asset.metadata?.contentType ?? "application/octet-stream",
};
}
async unstable_getByPathname(
pathname: string
): Promise<{ readableStream: ReadableStream; contentType: string } | null> {
const eTag = await this.unstable_exists(pathname);
if (!eTag) {
return null;
}
return this.unstable_getByETag(eTag);
}
async unstable_exists(pathname: string): Promise<string | null> {
const assetsManifest = new AssetsManifest(this.env.ASSETS_MANIFEST);
return await assetsManifest.get(pathname);
}
}