@uniflowed/server 0.0.0-alpha.8 → 0.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/edge.js +158 -0
- package/lambda.js +266 -0
- package/package.json +5 -1
package/edge.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/server/edge`: the Cloudflare Workers half of serving a build.
|
|
4
|
+
//
|
|
5
|
+
// [`./fetch.js`] answers the application's half of a request and touches no
|
|
6
|
+
// filesystem, because that is the half a worker runs. This is everything that
|
|
7
|
+
// is left on a worker, and there is much less of it than there is on Node: a
|
|
8
|
+
// worker has no socket to take and no directory to read, so what remains is
|
|
9
|
+
// the request lifecycle and the one decision `./node.js` makes with `stat` —
|
|
10
|
+
// does a file the build already wrote answer this, or does the application.
|
|
11
|
+
//
|
|
12
|
+
// # Where the static half went
|
|
13
|
+
//
|
|
14
|
+
// To the platform. Workers static assets are uploaded beside the script and
|
|
15
|
+
// served by Cloudflare's own asset server; the Worker reaches them through the
|
|
16
|
+
// `ASSETS` binding declared in `wrangler.json`, which answers a `Request` with
|
|
17
|
+
// a `Response` exactly as `createStaticHandler` does — and answers `404` where
|
|
18
|
+
// that returns `null`, which is the only translation this module performs.
|
|
19
|
+
//
|
|
20
|
+
// The Worker asks *first*, and `uf build --adapter edge` writes
|
|
21
|
+
// `"run_worker_first": true` so that it can. That looks like the wrong way
|
|
22
|
+
// round — Cloudflare's default is to serve a matching asset without invoking
|
|
23
|
+
// the script at all, which is faster and resolves a file/handler collision the
|
|
24
|
+
// same way — but the default also puts the resolution order in a platform
|
|
25
|
+
// setting rather than in uf, and `tests/library/deploy.test.js` cannot drive a
|
|
26
|
+
// platform setting. Asking here means one order, written once, checked by the
|
|
27
|
+
// test that checks `uf start`'s. See ubugeeei-prod/uf#391.
|
|
28
|
+
//
|
|
29
|
+
// # Who owns the request, and the one thing a worker cannot promise
|
|
30
|
+
//
|
|
31
|
+
// `after()` says "once the response has been sent", and there is no such line
|
|
32
|
+
// in a worker: [`createWorkerFetch`] has a `Response` in hand, and the platform
|
|
33
|
+
// writes its body after the handler has returned. `ctx.waitUntil` is the
|
|
34
|
+
// documented way to keep the isolate alive for work that outlives the handler,
|
|
35
|
+
// so that is what `settle` is handed — which means an `after()` callback on
|
|
36
|
+
// this target begins when the response has been *decided* rather than when its
|
|
37
|
+
// last byte is out. For a streamed document those are a document apart. It is
|
|
38
|
+
// written down here, in `docs/app/reference/cli/_uf.page.mdx`, and it is the
|
|
39
|
+
// one behavioural difference between this target and the other three.
|
|
40
|
+
//
|
|
41
|
+
// `beginRequest` is passed in rather than imported, for the reason `./node.js`
|
|
42
|
+
// gives at length: the request lives in an `AsyncLocalStorage` belonging to a
|
|
43
|
+
// module instance, and the instance the application reads is the one bundled
|
|
44
|
+
// into the `handler.js` beside the generated `worker.js`. See
|
|
45
|
+
// ubugeeei-prod/uf#389.
|
|
46
|
+
|
|
47
|
+
import type { RequestLifecycle } from "./internal/context.js";
|
|
48
|
+
|
|
49
|
+
export type { RequestLifecycle } from "./internal/context.js";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `ASSETS` binding, as much of it as this module uses.
|
|
53
|
+
*
|
|
54
|
+
* One method, declared structurally: a worker's bindings are supplied by the
|
|
55
|
+
* runtime, and naming the whole of Cloudflare's `Fetcher` here would be this
|
|
56
|
+
* package holding a copy of another project's types.
|
|
57
|
+
*/
|
|
58
|
+
export type AssetsBinding = {
|
|
59
|
+
readonly fetch: (request: Request) => Promise<Response>,
|
|
60
|
+
...
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The `env` a Worker's `fetch` is called with.
|
|
65
|
+
*
|
|
66
|
+
* Inexact, because a project's own bindings — a KV namespace, a secret — are
|
|
67
|
+
* in here too and are none of this module's business. `ASSETS` is optional
|
|
68
|
+
* because a Worker deployed without an assets directory has no such binding,
|
|
69
|
+
* and the honest answer for that deployment is "the application answers
|
|
70
|
+
* everything" rather than a `TypeError` on the first request.
|
|
71
|
+
*/
|
|
72
|
+
export type EdgeEnvironment = {
|
|
73
|
+
readonly ASSETS?: AssetsBinding,
|
|
74
|
+
...
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The `ctx` a Worker's `fetch` is called with.
|
|
79
|
+
*
|
|
80
|
+
* `waitUntil` only. `passThroughOnException` exists and is deliberately not
|
|
81
|
+
* used: it serves the origin's response when the script throws, and a Worker
|
|
82
|
+
* that *is* the origin has nothing to pass through to.
|
|
83
|
+
*/
|
|
84
|
+
export type ExecutionContext = {
|
|
85
|
+
readonly waitUntil: (promise: Promise<mixed>) => mixed,
|
|
86
|
+
...
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Everything the worker half needs to answer a request. */
|
|
90
|
+
export type WorkerHandlerOptions = {|
|
|
91
|
+
/** The application, from the generated `handler.js`. */
|
|
92
|
+
readonly handle: (request: Request) => Promise<Response>,
|
|
93
|
+
/** That same module's `beginRequest`; see the header. */
|
|
94
|
+
readonly beginRequest: (request: Request) => RequestLifecycle,
|
|
95
|
+
|};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A built uf application as a Worker's `fetch`.
|
|
99
|
+
*
|
|
100
|
+
* Static assets first, then the application — the order `uf preview` cannot
|
|
101
|
+
* deviate from and therefore the order every other front door matches. The
|
|
102
|
+
* asset lookup is skipped for anything that is not a `GET` or a `HEAD`, which
|
|
103
|
+
* is what `createStaticHandler` does and for the same reason: a `POST` to a
|
|
104
|
+
* path that happens to have a file under it belongs to a route handler.
|
|
105
|
+
*
|
|
106
|
+
* A `404` from the assets binding means "no such asset", not "the site has no
|
|
107
|
+
* such page": `wrangler.json` sets `"not_found_handling": "none"` so that the
|
|
108
|
+
* miss falls through to here, and the 404 a visitor sees is the project's own
|
|
109
|
+
* `_uf.not-found` rendered by the application. Any other status is the asset's
|
|
110
|
+
* answer and is returned as it stands.
|
|
111
|
+
*/
|
|
112
|
+
export function createWorkerFetch(
|
|
113
|
+
options: WorkerHandlerOptions,
|
|
114
|
+
): (request: Request, env: EdgeEnvironment, ctx?: ExecutionContext) => Promise<Response> {
|
|
115
|
+
const { handle, beginRequest } = options;
|
|
116
|
+
|
|
117
|
+
return async function fetchFromWorker(
|
|
118
|
+
request: Request,
|
|
119
|
+
env: EdgeEnvironment,
|
|
120
|
+
ctx?: ExecutionContext,
|
|
121
|
+
): Promise<Response> {
|
|
122
|
+
const lifecycle = beginRequest(request);
|
|
123
|
+
try {
|
|
124
|
+
return await lifecycle.run(async () => {
|
|
125
|
+
const assets = env?.ASSETS;
|
|
126
|
+
const method = request.method.toUpperCase();
|
|
127
|
+
if (assets != null && (method === "GET" || method === "HEAD")) {
|
|
128
|
+
const asset = await assets.fetch(request);
|
|
129
|
+
if (asset.status !== 404) return asset;
|
|
130
|
+
}
|
|
131
|
+
return await handle(request);
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// The same 500 `./node.js`'s `nodeListener` writes, and for the same
|
|
135
|
+
// reasons: the body must not carry the stack, because the body goes to
|
|
136
|
+
// whoever asked, and the console is where the operator is already
|
|
137
|
+
// looking. Without this the answer would be Cloudflare's own error page,
|
|
138
|
+
// which is a different answer from `uf start`'s for the same failure —
|
|
139
|
+
// and the whole claim of the seam is that there is one answer.
|
|
140
|
+
console.error(error);
|
|
141
|
+
return new Response("500 Internal Server Error\n", {
|
|
142
|
+
status: 500,
|
|
143
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
144
|
+
});
|
|
145
|
+
} finally {
|
|
146
|
+
// Scheduled rather than awaited: awaiting it here would hold the
|
|
147
|
+
// response back until every `after()` callback had finished, which is
|
|
148
|
+
// the opposite of what `after()` is for. Where there is no `ctx` — a
|
|
149
|
+
// test, or a host that calls this directly — it is awaited, because
|
|
150
|
+
// dropping the promise would lose both the work and its rejection.
|
|
151
|
+
if (ctx != null) {
|
|
152
|
+
ctx.waitUntil(lifecycle.settle());
|
|
153
|
+
} else {
|
|
154
|
+
await lifecycle.settle();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
package/lambda.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/server/lambda`: the AWS Lambda half of serving a build.
|
|
4
|
+
//
|
|
5
|
+
// A serverless target has no server. What it has is one function per
|
|
6
|
+
// invocation, whose signature belongs to the platform rather than to the Web —
|
|
7
|
+
// so this module is a translation, and the thing being translated is the same
|
|
8
|
+
// [`./fetch.js`] handler `uf start` and every other front door answer through.
|
|
9
|
+
//
|
|
10
|
+
// # One event shape, named
|
|
11
|
+
//
|
|
12
|
+
// **Payload format version 2.0**, which is what a Lambda Function URL always
|
|
13
|
+
// sends and what an API Gateway *HTTP* API sends by default. It is not what an
|
|
14
|
+
// API Gateway *REST* API sends (that is format 1.0, with `httpMethod`,
|
|
15
|
+
// `path` and `multiValueHeaders`), and it is not an ALB target-group event.
|
|
16
|
+
// Supporting all three by sniffing the event would be three untested code
|
|
17
|
+
// paths where the platform's own documentation says which one you get; this
|
|
18
|
+
// one is named in `docs/app/reference/cli/_uf.page.mdx`, and an event that is
|
|
19
|
+
// not it is refused by [`toRequest`] with a message saying so rather than
|
|
20
|
+
// answered from fields that happen to be undefined.
|
|
21
|
+
//
|
|
22
|
+
// # Buffered, and why that is a limitation rather than a choice
|
|
23
|
+
//
|
|
24
|
+
// `createFetchHandler` answers a document as a stream, so a `<Suspense>`
|
|
25
|
+
// fallback reaches the browser while the page behind it is still resolving.
|
|
26
|
+
// A Lambda response in this format is a JSON value, so the whole body is read
|
|
27
|
+
// before the invocation returns and none of that streaming survives. Response
|
|
28
|
+
// streaming exists — `awslambda.streamifyResponse`, on a Function URL whose
|
|
29
|
+
// invoke mode is `RESPONSE_STREAM` — and this module does not implement it:
|
|
30
|
+
// the wrapper is a global the managed runtime injects, so nothing outside a
|
|
31
|
+
// real invocation can drive it, and an untested streaming path is worse than a
|
|
32
|
+
// buffered one that says it is buffered. The 6 MB response payload limit is
|
|
33
|
+
// the platform's and applies to what this returns.
|
|
34
|
+
//
|
|
35
|
+
// # The static half is here rather than on a CDN, unless you put one there
|
|
36
|
+
//
|
|
37
|
+
// `staticDir` is served through `./node.js`'s `createStaticHandler`, from the
|
|
38
|
+
// deployment package itself, so the function answers a prerendered document
|
|
39
|
+
// and a hashed asset without any other infrastructure existing. That is the
|
|
40
|
+
// shape that works the moment it is uploaded; it is not the shape anybody
|
|
41
|
+
// should keep. Every byte of `static/` is then billed as invocation time and
|
|
42
|
+
// counted against the package limit, and a CloudFront distribution or an S3
|
|
43
|
+
// origin in front of the function is what the platform expects. Leaving
|
|
44
|
+
// `staticDir` out is how you say you have one — the application half then
|
|
45
|
+
// answers alone, exactly as it does on a worker.
|
|
46
|
+
|
|
47
|
+
import { Buffer } from "node:buffer";
|
|
48
|
+
|
|
49
|
+
import { createStaticHandler } from "./node.js";
|
|
50
|
+
|
|
51
|
+
import type { RequestLifecycle } from "./internal/context.js";
|
|
52
|
+
|
|
53
|
+
export type { RequestLifecycle } from "./internal/context.js";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An HTTP API payload format 2.0 event, as much of it as this module reads.
|
|
57
|
+
*
|
|
58
|
+
* Inexact and almost entirely optional, because it arrives from the platform
|
|
59
|
+
* rather than from a caller: the fields uf needs are checked in [`toRequest`],
|
|
60
|
+
* where a missing one can be reported as the wrong event shape.
|
|
61
|
+
*/
|
|
62
|
+
export type LambdaHttpEvent = {
|
|
63
|
+
readonly version?: string,
|
|
64
|
+
readonly rawPath?: string,
|
|
65
|
+
readonly rawQueryString?: string,
|
|
66
|
+
readonly cookies?: $ReadOnlyArray<string>,
|
|
67
|
+
readonly headers?: { readonly [string]: string | void },
|
|
68
|
+
readonly body?: string,
|
|
69
|
+
readonly isBase64Encoded?: boolean,
|
|
70
|
+
readonly requestContext?: {
|
|
71
|
+
readonly domainName?: string,
|
|
72
|
+
readonly http?: {
|
|
73
|
+
readonly method?: string,
|
|
74
|
+
readonly path?: string,
|
|
75
|
+
...
|
|
76
|
+
},
|
|
77
|
+
...
|
|
78
|
+
},
|
|
79
|
+
...
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** What Lambda expects back for payload format 2.0. */
|
|
83
|
+
export type LambdaHttpResult = {|
|
|
84
|
+
readonly statusCode: number,
|
|
85
|
+
readonly headers: { [string]: string },
|
|
86
|
+
readonly cookies: $ReadOnlyArray<string>,
|
|
87
|
+
readonly body: string,
|
|
88
|
+
readonly isBase64Encoded: boolean,
|
|
89
|
+
|};
|
|
90
|
+
|
|
91
|
+
/** Everything the serverless half needs to answer an invocation. */
|
|
92
|
+
export type LambdaHandlerOptions = {|
|
|
93
|
+
/** The application, from the generated `handler.js`. */
|
|
94
|
+
readonly handle: (request: Request) => Promise<Response>,
|
|
95
|
+
/** That same module's `beginRequest`; see [`./node.js`]'s header for why. */
|
|
96
|
+
readonly beginRequest: (request: Request) => RequestLifecycle,
|
|
97
|
+
/**
|
|
98
|
+
* The directory holding the build's own files, if the package carries them.
|
|
99
|
+
*
|
|
100
|
+
* Omitted where a CDN answers for them; see the header.
|
|
101
|
+
*/
|
|
102
|
+
readonly staticDir?: string,
|
|
103
|
+
|};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The `getSetCookie` half of `Headers`.
|
|
107
|
+
*
|
|
108
|
+
* Declared rather than called straight off the value because iterating
|
|
109
|
+
* `Headers` joins repeated fields with a comma, and `Set-Cookie` is the one
|
|
110
|
+
* field where that is a corruption rather than a spelling: two cookies become
|
|
111
|
+
* one header nothing can parse back apart. `getSetCookie` is the standard
|
|
112
|
+
* answer and every runtime that can run this has it — but a runtime that does
|
|
113
|
+
* not would throw here rather than at the point the cookies were set, so the
|
|
114
|
+
* absence is checked.
|
|
115
|
+
*/
|
|
116
|
+
type SetCookieReader = {
|
|
117
|
+
readonly getSetCookie?: () => $ReadOnlyArray<string>,
|
|
118
|
+
...
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Media types whose bodies are text, and therefore not base64.
|
|
123
|
+
*
|
|
124
|
+
* The same shape of decision `./node.js` makes with `CONTENT_TYPES`, and the
|
|
125
|
+
* same default: what is not known to be text is returned base64-encoded, which
|
|
126
|
+
* is lossless for every byte sequence. Getting it the other way round would
|
|
127
|
+
* corrupt an image silently.
|
|
128
|
+
*/
|
|
129
|
+
const TEXT_TYPES: $ReadOnlyArray<string> = Object.freeze([
|
|
130
|
+
"application/javascript",
|
|
131
|
+
"application/json",
|
|
132
|
+
"application/manifest+json",
|
|
133
|
+
"application/xml",
|
|
134
|
+
"image/svg+xml",
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
/** Whether a response with this `content-type` can be returned as a string. */
|
|
138
|
+
function isText(contentType: string | null): boolean {
|
|
139
|
+
if (contentType == null) return false;
|
|
140
|
+
const media = contentType.split(";")[0].trim().toLowerCase();
|
|
141
|
+
if (media.startsWith("text/")) return true;
|
|
142
|
+
if (media.endsWith("+json") || media.endsWith("+xml")) return true;
|
|
143
|
+
return TEXT_TYPES.includes(media);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The event as a `Request`.
|
|
148
|
+
*
|
|
149
|
+
* The URL is rebuilt rather than taken from a field, because no field holds
|
|
150
|
+
* one: the path is `rawPath`, the query is `rawQueryString`, and the authority
|
|
151
|
+
* is the `host` header the client sent — falling back to the API's own domain,
|
|
152
|
+
* which is what a health check with no `Host` arrives with. `https`, always:
|
|
153
|
+
* both a Function URL and an HTTP API terminate TLS, and there is no spelling
|
|
154
|
+
* of either that a browser reaches over `http`.
|
|
155
|
+
*
|
|
156
|
+
* Cookies come from `event.cookies` and not from a header, because that is
|
|
157
|
+
* where format 2.0 puts them — an application reading `cookies()` would
|
|
158
|
+
* otherwise see none of them, which is the kind of difference between a
|
|
159
|
+
* deployment and `uf start` this whole seam exists to prevent.
|
|
160
|
+
*/
|
|
161
|
+
export function toRequest(event: LambdaHttpEvent): Request {
|
|
162
|
+
const method = event.requestContext?.http?.method;
|
|
163
|
+
const rawPath = event.rawPath;
|
|
164
|
+
if (typeof method !== "string" || typeof rawPath !== "string") {
|
|
165
|
+
throw new Error(
|
|
166
|
+
"uf: this handler reads AWS Lambda payload format 2.0 — a Lambda Function URL, " +
|
|
167
|
+
"or an API Gateway HTTP API — and the event it was given has no " +
|
|
168
|
+
"`requestContext.http.method` and `rawPath`. An API Gateway REST API sends " +
|
|
169
|
+
"format 1.0 and an ALB sends its own shape; neither is supported.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const headers = new Headers();
|
|
174
|
+
const source = event.headers ?? {};
|
|
175
|
+
for (const name of Object.keys(source)) {
|
|
176
|
+
const value = source[name];
|
|
177
|
+
if (typeof value === "string") headers.set(name, value);
|
|
178
|
+
}
|
|
179
|
+
const cookies = event.cookies ?? [];
|
|
180
|
+
if (cookies.length > 0) headers.set("cookie", cookies.join("; "));
|
|
181
|
+
|
|
182
|
+
const authority =
|
|
183
|
+
headers.get("host") ?? event.requestContext?.domainName ?? "lambda.amazonaws.com";
|
|
184
|
+
const query = event.rawQueryString ?? "";
|
|
185
|
+
const url = new URL(`https://${authority}${rawPath}${query === "" ? "" : `?${query}`}`);
|
|
186
|
+
|
|
187
|
+
const init: { [string]: mixed } = { method: method.toUpperCase(), headers };
|
|
188
|
+
// `Request` refuses a body on a `GET` or a `HEAD`, and API Gateway is under
|
|
189
|
+
// no obligation not to send one: a client can, and the invocation would then
|
|
190
|
+
// fail with a `TypeError` from the constructor rather than answer.
|
|
191
|
+
if (typeof event.body === "string" && !["GET", "HEAD"].includes(method.toUpperCase())) {
|
|
192
|
+
init.body =
|
|
193
|
+
event.isBase64Encoded === true ? Buffer.from(event.body, "base64") : Buffer.from(event.body);
|
|
194
|
+
}
|
|
195
|
+
return new Request(url, init);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A `Response` as the JSON value Lambda returns to the client. */
|
|
199
|
+
export async function toResult(response: Response): Promise<LambdaHttpResult> {
|
|
200
|
+
const headers: { [string]: string } = {};
|
|
201
|
+
for (const [name, value] of response.headers) {
|
|
202
|
+
// Skipped here and carried in `cookies` below: see [`SetCookieReader`].
|
|
203
|
+
if (name.toLowerCase() === "set-cookie") continue;
|
|
204
|
+
headers[name] = value;
|
|
205
|
+
}
|
|
206
|
+
const reader: SetCookieReader = response.headers;
|
|
207
|
+
const cookies = typeof reader.getSetCookie === "function" ? [...reader.getSetCookie()] : [];
|
|
208
|
+
|
|
209
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
210
|
+
const text = isText(response.headers.get("content-type"));
|
|
211
|
+
return {
|
|
212
|
+
statusCode: response.status,
|
|
213
|
+
headers,
|
|
214
|
+
cookies,
|
|
215
|
+
body: text ? body.toString("utf8") : body.toString("base64"),
|
|
216
|
+
isBase64Encoded: !text,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A built uf application as a Lambda handler.
|
|
222
|
+
*
|
|
223
|
+
* Static files first, then the application — the order `uf preview` cannot
|
|
224
|
+
* deviate from and therefore the order every other front door matches. The
|
|
225
|
+
* request is begun here and settled once the body has been read into the
|
|
226
|
+
* result, which on this target is genuinely "the response has been produced":
|
|
227
|
+
* an invocation that returned before its `after()` callbacks ran would have
|
|
228
|
+
* them killed with the sandbox, so `settle` is awaited rather than deferred.
|
|
229
|
+
*/
|
|
230
|
+
export function createLambdaHandler(
|
|
231
|
+
options: LambdaHandlerOptions,
|
|
232
|
+
): (event: LambdaHttpEvent) => Promise<LambdaHttpResult> {
|
|
233
|
+
const { handle, beginRequest, staticDir } = options;
|
|
234
|
+
const serveStatic = staticDir == null ? null : createStaticHandler({ root: staticDir });
|
|
235
|
+
|
|
236
|
+
return async function lambdaHandler(event: LambdaHttpEvent): Promise<LambdaHttpResult> {
|
|
237
|
+
const request = toRequest(event);
|
|
238
|
+
const lifecycle = beginRequest(request);
|
|
239
|
+
try {
|
|
240
|
+
return await lifecycle.run(async () => {
|
|
241
|
+
const asset = serveStatic == null ? null : await serveStatic(request);
|
|
242
|
+
return await toResult(asset ?? (await handle(request)));
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
// The same 500 `./node.js`'s `nodeListener` writes, and for the same
|
|
246
|
+
// reasons: the body must not carry the stack, and the console — which on
|
|
247
|
+
// Lambda is CloudWatch — is where the operator is already looking. A
|
|
248
|
+
// rejected invocation would be a 502 from API Gateway instead, which is
|
|
249
|
+
// a different answer from `uf start`'s for the same failure.
|
|
250
|
+
//
|
|
251
|
+
// `toRequest` above is deliberately outside this: an event in the wrong
|
|
252
|
+
// format is a misconfigured function rather than a failed request, and
|
|
253
|
+
// answering it 500 forever would hide that.
|
|
254
|
+
console.error(error);
|
|
255
|
+
return {
|
|
256
|
+
statusCode: 500,
|
|
257
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
258
|
+
cookies: [],
|
|
259
|
+
body: "500 Internal Server Error\n",
|
|
260
|
+
isBase64Encoded: false,
|
|
261
|
+
};
|
|
262
|
+
} finally {
|
|
263
|
+
await lifecycle.settle();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/server",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.9",
|
|
4
4
|
"description": "Request-scoped server functions for the Unified Toolchain for Flow (React).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,15 +12,19 @@
|
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./index.js",
|
|
15
|
+
"./edge": "./edge.js",
|
|
15
16
|
"./fetch": "./fetch.js",
|
|
16
17
|
"./host": "./host.js",
|
|
18
|
+
"./lambda": "./lambda.js",
|
|
17
19
|
"./node": "./node.js",
|
|
18
20
|
"./standalone": "./standalone.js"
|
|
19
21
|
},
|
|
20
22
|
"files": [
|
|
23
|
+
"edge.js",
|
|
21
24
|
"fetch.js",
|
|
22
25
|
"index.js",
|
|
23
26
|
"host.js",
|
|
27
|
+
"lambda.js",
|
|
24
28
|
"node.js",
|
|
25
29
|
"standalone.js",
|
|
26
30
|
"internal/*.js"
|