@beesolve/lambda-bun-runtime 1.2.0 → 1.2.1
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/.jsii +2 -2
- package/CHANGELOG.md +8 -0
- package/lib/bun-lambda-layer-1.3.3.zip +0 -0
- package/lib/index.js +2 -2
- package/package.json +1 -1
- package/command/buildLayer.sh +0 -25
- package/command/runtime.ts +0 -927
package/.jsii
CHANGED
|
@@ -8598,6 +8598,6 @@
|
|
|
8598
8598
|
"symbolId": "src/index:BunLambdaLayerProps"
|
|
8599
8599
|
}
|
|
8600
8600
|
},
|
|
8601
|
-
"version": "1.2.
|
|
8602
|
-
"fingerprint": "
|
|
8601
|
+
"version": "1.2.1",
|
|
8602
|
+
"fingerprint": "iBOCsncYD+3MPsQg4J3Nr5UOjdhOW/H9xYQgV5Uyq88="
|
|
8603
8603
|
}
|
package/CHANGELOG.md
ADDED
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,7 @@ class BunFunction extends aws_lambda_1.Function {
|
|
|
21
21
|
}
|
|
22
22
|
exports.BunFunction = BunFunction;
|
|
23
23
|
_a = JSII_RTTI_SYMBOL_1;
|
|
24
|
-
BunFunction[_a] = { fqn: "@beesolve/lambda-bun-runtime.BunFunction", version: "1.2.
|
|
24
|
+
BunFunction[_a] = { fqn: "@beesolve/lambda-bun-runtime.BunFunction", version: "1.2.1" };
|
|
25
25
|
class BunLambdaLayer extends aws_lambda_1.LayerVersion {
|
|
26
26
|
constructor(scope, id, props) {
|
|
27
27
|
super(scope, id, {
|
|
@@ -37,7 +37,7 @@ class BunLambdaLayer extends aws_lambda_1.LayerVersion {
|
|
|
37
37
|
}
|
|
38
38
|
exports.BunLambdaLayer = BunLambdaLayer;
|
|
39
39
|
_b = JSII_RTTI_SYMBOL_1;
|
|
40
|
-
BunLambdaLayer[_b] = { fqn: "@beesolve/lambda-bun-runtime.BunLambdaLayer", version: "1.2.
|
|
40
|
+
BunLambdaLayer[_b] = { fqn: "@beesolve/lambda-bun-runtime.BunLambdaLayer", version: "1.2.1" };
|
|
41
41
|
function toEntry(entrypoint) {
|
|
42
42
|
const entry = entrypoint.split("/").pop()?.split(".").shift();
|
|
43
43
|
if (entry == null)
|
package/package.json
CHANGED
package/command/buildLayer.sh
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
set -e
|
|
4
|
-
|
|
5
|
-
VERSION="1.3.3"
|
|
6
|
-
TAG="bun-v$VERSION"
|
|
7
|
-
|
|
8
|
-
TMPDIR=${TMPDIR:-/tmp}
|
|
9
|
-
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
10
|
-
|
|
11
|
-
cd "$TMPDIR" && \
|
|
12
|
-
mkdir bun-layer && \
|
|
13
|
-
cd bun-layer && \
|
|
14
|
-
git clone --filter=blob:none --sparse https://github.com/oven-sh/bun.git && \
|
|
15
|
-
cd bun && \
|
|
16
|
-
git checkout "$TAG" && \
|
|
17
|
-
git sparse-checkout set packages/bun-lambda && \
|
|
18
|
-
cd packages/bun-lambda && \
|
|
19
|
-
cp "$SCRIPT_DIR/runtime.ts" . && \
|
|
20
|
-
bun install && \
|
|
21
|
-
bun run build-layer && \
|
|
22
|
-
mv ./bun-lambda-layer.zip "$SCRIPT_DIR/../src/bun-lambda-layer-$VERSION.zip" && \
|
|
23
|
-
cd - && \
|
|
24
|
-
cd ../.. && \
|
|
25
|
-
rm -rf bun-layer
|
package/command/runtime.ts
DELETED
|
@@ -1,927 +0,0 @@
|
|
|
1
|
-
import { AwsClient } from "aws4fetch";
|
|
2
|
-
import type { Server, ServerWebSocket } from "bun";
|
|
3
|
-
|
|
4
|
-
type Lambda = {
|
|
5
|
-
fetch: (request: Request, server: Server) => Promise<Response | undefined>;
|
|
6
|
-
error?: (error: unknown) => Promise<Response>;
|
|
7
|
-
websocket?: {
|
|
8
|
-
open?: (ws: ServerWebSocket) => Promise<void>;
|
|
9
|
-
message?: (ws: ServerWebSocket, message: string) => Promise<void>;
|
|
10
|
-
close?: (
|
|
11
|
-
ws: ServerWebSocket,
|
|
12
|
-
code: number,
|
|
13
|
-
reason: string,
|
|
14
|
-
) => Promise<void>;
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
let requestId: string | undefined;
|
|
19
|
-
let traceId: string | undefined;
|
|
20
|
-
let functionArn: string | undefined;
|
|
21
|
-
let aws: AwsClient | undefined;
|
|
22
|
-
|
|
23
|
-
let logger = console.log;
|
|
24
|
-
|
|
25
|
-
function log(level: string, ...args: any[]): void {
|
|
26
|
-
if (!args.length) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const messages = args.map((arg) => Bun.inspect(arg).replace(/\n/g, "\r"));
|
|
30
|
-
if (requestId === undefined) {
|
|
31
|
-
logger(level, ...messages);
|
|
32
|
-
} else {
|
|
33
|
-
logger(level, `RequestId: ${requestId}`, ...messages);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
console.log = (...args: any[]) => log("INFO", ...args);
|
|
38
|
-
console.info = (...args: any[]) => log("INFO", ...args);
|
|
39
|
-
console.warn = (...args: any[]) => log("WARN", ...args);
|
|
40
|
-
console.error = (...args: any[]) => log("ERROR", ...args);
|
|
41
|
-
console.debug = (...args: any[]) => log("DEBUG", ...args);
|
|
42
|
-
console.trace = (...args: any[]) => log("TRACE", ...args);
|
|
43
|
-
|
|
44
|
-
let warnings: Set<string> | undefined;
|
|
45
|
-
|
|
46
|
-
function warnOnce(message: string, ...args: any[]): void {
|
|
47
|
-
if (warnings === undefined) {
|
|
48
|
-
warnings = new Set();
|
|
49
|
-
}
|
|
50
|
-
if (warnings.has(message)) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
warnings.add(message);
|
|
54
|
-
console.warn(message, ...args);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function reset(): void {
|
|
58
|
-
requestId = undefined;
|
|
59
|
-
traceId = undefined;
|
|
60
|
-
warnings = undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function exit(...cause: any[]): never {
|
|
64
|
-
console.error(...cause);
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function env(name: string, fallback?: string): string {
|
|
69
|
-
const value = process.env[name] ?? fallback ?? null;
|
|
70
|
-
if (value === null) {
|
|
71
|
-
exit(`Runtime failed to find the '${name}' environment variable`);
|
|
72
|
-
}
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const runtimeUrl = new URL(
|
|
77
|
-
`http://${env("AWS_LAMBDA_RUNTIME_API")}/2018-06-01/`,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
async function fetch(url: string, options?: RequestInit): Promise<Response> {
|
|
81
|
-
const { href } = new URL(url, runtimeUrl);
|
|
82
|
-
const response = await globalThis.fetch(href, {
|
|
83
|
-
...options,
|
|
84
|
-
timeout: false,
|
|
85
|
-
});
|
|
86
|
-
if (!response.ok) {
|
|
87
|
-
exit(
|
|
88
|
-
`Runtime failed to send request to Lambda [status: ${response.status}]`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
return response;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function fetchAws(url: string, options?: RequestInit): Promise<Response> {
|
|
95
|
-
if (aws === undefined) {
|
|
96
|
-
aws = new AwsClient({
|
|
97
|
-
accessKeyId: env("AWS_ACCESS_KEY_ID"),
|
|
98
|
-
secretAccessKey: env("AWS_SECRET_ACCESS_KEY"),
|
|
99
|
-
sessionToken: env("AWS_SESSION_TOKEN"),
|
|
100
|
-
region: env("AWS_REGION"),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return aws.fetch(url, options);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type LambdaError = {
|
|
107
|
-
readonly errorType: string;
|
|
108
|
-
readonly errorMessage: string;
|
|
109
|
-
readonly stackTrace?: string[];
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
function formatError(error: unknown): LambdaError {
|
|
113
|
-
if (error instanceof Error) {
|
|
114
|
-
return {
|
|
115
|
-
errorType: error.name,
|
|
116
|
-
errorMessage: error.message,
|
|
117
|
-
stackTrace: error.stack
|
|
118
|
-
?.split("\n")
|
|
119
|
-
.filter((line) => !line.includes(" /opt/runtime.ts")),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
errorType: "Error",
|
|
124
|
-
errorMessage: Bun.inspect(error),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function sendError(type: string, cause: unknown): Promise<void> {
|
|
129
|
-
console.error(cause);
|
|
130
|
-
await fetch(
|
|
131
|
-
requestId === undefined
|
|
132
|
-
? "runtime/init/error"
|
|
133
|
-
: `runtime/invocation/${requestId}/error`,
|
|
134
|
-
{
|
|
135
|
-
method: "POST",
|
|
136
|
-
headers: {
|
|
137
|
-
"Content-Type": "application/vnd.aws.lambda.error+json",
|
|
138
|
-
"Lambda-Runtime-Function-Error-Type": `Bun.${type}`,
|
|
139
|
-
},
|
|
140
|
-
body: JSON.stringify(formatError(cause)),
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function throwError(type: string, cause: unknown): Promise<never> {
|
|
146
|
-
await sendError(type, cause);
|
|
147
|
-
exit();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function init(): Promise<Lambda> {
|
|
151
|
-
const handlerName = env("_HANDLER");
|
|
152
|
-
const index = handlerName.lastIndexOf(".");
|
|
153
|
-
const fileName = handlerName.substring(0, index);
|
|
154
|
-
const filePath = `${env("LAMBDA_TASK_ROOT")}/${fileName}`;
|
|
155
|
-
let file;
|
|
156
|
-
try {
|
|
157
|
-
file = await import(filePath);
|
|
158
|
-
} catch (cause) {
|
|
159
|
-
if (
|
|
160
|
-
cause instanceof Error &&
|
|
161
|
-
cause.message.startsWith("Cannot find module")
|
|
162
|
-
) {
|
|
163
|
-
return throwError(
|
|
164
|
-
"FileDoesNotExist",
|
|
165
|
-
`Did not find a file named '${fileName}'`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
return throwError("InitError", cause);
|
|
169
|
-
}
|
|
170
|
-
const moduleName = handlerName.substring(index + 1) || "fetch";
|
|
171
|
-
let module = file["default"] ?? file[moduleName] ?? {};
|
|
172
|
-
if (typeof module === "function") {
|
|
173
|
-
module = {
|
|
174
|
-
fetch: module,
|
|
175
|
-
};
|
|
176
|
-
} else if (typeof module === "object" && moduleName !== "fetch") {
|
|
177
|
-
module = {
|
|
178
|
-
...module,
|
|
179
|
-
fetch: module[moduleName],
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
const { fetch, websocket } = module;
|
|
183
|
-
if (typeof fetch !== "function") {
|
|
184
|
-
return throwError(
|
|
185
|
-
fetch === undefined ? "MethodDoesNotExist" : "MethodIsNotAFunction",
|
|
186
|
-
`${fileName} does not have a default export with a function named '${moduleName}'`,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
if (websocket === undefined) {
|
|
190
|
-
return module;
|
|
191
|
-
}
|
|
192
|
-
for (const name of ["open", "message", "close"]) {
|
|
193
|
-
const method = websocket[name];
|
|
194
|
-
if (method === undefined) {
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (typeof method !== "function") {
|
|
198
|
-
return throwError(
|
|
199
|
-
"MethodIsNotAFunction",
|
|
200
|
-
`${fileName} does not have a function named '${name}' on the default 'websocket' property`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return module;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
type LambdaRequest<E = any> = {
|
|
208
|
-
readonly requestId: string;
|
|
209
|
-
readonly traceId: string;
|
|
210
|
-
readonly functionArn: string;
|
|
211
|
-
readonly deadlineMs: number | null;
|
|
212
|
-
readonly event: E;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
async function receiveRequest(): Promise<LambdaRequest> {
|
|
216
|
-
const response = await fetch("runtime/invocation/next");
|
|
217
|
-
requestId =
|
|
218
|
-
response.headers.get("Lambda-Runtime-Aws-Request-Id") ?? undefined;
|
|
219
|
-
if (requestId === undefined) {
|
|
220
|
-
exit("Runtime received a request without a request ID");
|
|
221
|
-
}
|
|
222
|
-
traceId = response.headers.get("Lambda-Runtime-Trace-Id") ?? undefined;
|
|
223
|
-
if (traceId === undefined) {
|
|
224
|
-
exit("Runtime received a request without a trace ID");
|
|
225
|
-
}
|
|
226
|
-
process.env["_X_AMZN_TRACE_ID"] = traceId;
|
|
227
|
-
functionArn =
|
|
228
|
-
response.headers.get("Lambda-Runtime-Invoked-Function-Arn") ?? undefined;
|
|
229
|
-
if (functionArn === undefined) {
|
|
230
|
-
exit("Runtime received a request without a function ARN");
|
|
231
|
-
}
|
|
232
|
-
const deadlineMs =
|
|
233
|
-
parseInt(response.headers.get("Lambda-Runtime-Deadline-Ms") ?? "0") || null;
|
|
234
|
-
let event;
|
|
235
|
-
try {
|
|
236
|
-
event = await response.json();
|
|
237
|
-
} catch (cause) {
|
|
238
|
-
exit("Runtime received a request with invalid JSON", cause);
|
|
239
|
-
}
|
|
240
|
-
return {
|
|
241
|
-
requestId,
|
|
242
|
-
traceId,
|
|
243
|
-
functionArn,
|
|
244
|
-
deadlineMs,
|
|
245
|
-
event,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
type LambdaResponse = {
|
|
250
|
-
readonly statusCode: number;
|
|
251
|
-
readonly headers?: Record<string, string>;
|
|
252
|
-
readonly isBase64Encoded?: boolean;
|
|
253
|
-
readonly body?: string;
|
|
254
|
-
readonly multiValueHeaders?: Record<string, string[]>;
|
|
255
|
-
readonly cookies?: string[];
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
async function formatResponse(
|
|
259
|
-
response: Response,
|
|
260
|
-
eventType: "v1" | "v2",
|
|
261
|
-
): Promise<LambdaResponse> {
|
|
262
|
-
const statusCode = response.status;
|
|
263
|
-
const headers = response.headers.toJSON();
|
|
264
|
-
if (statusCode === 101) {
|
|
265
|
-
const protocol = headers["sec-websocket-protocol"];
|
|
266
|
-
if (protocol === undefined) {
|
|
267
|
-
return {
|
|
268
|
-
statusCode: 200,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
return {
|
|
272
|
-
statusCode: 200,
|
|
273
|
-
headers: {
|
|
274
|
-
"Sec-WebSocket-Protocol": protocol,
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
const mime = headers["content-type"];
|
|
279
|
-
const isBase64Encoded =
|
|
280
|
-
!mime ||
|
|
281
|
-
(!mime.startsWith("text/") && !mime.startsWith("application/json"));
|
|
282
|
-
const body = isBase64Encoded
|
|
283
|
-
? Buffer.from(await response.arrayBuffer()).toString("base64")
|
|
284
|
-
: await response.text();
|
|
285
|
-
delete headers["set-cookie"];
|
|
286
|
-
const cookies = response.headers.getAll("Set-Cookie");
|
|
287
|
-
if (cookies.length === 0) {
|
|
288
|
-
return {
|
|
289
|
-
statusCode,
|
|
290
|
-
headers,
|
|
291
|
-
isBase64Encoded,
|
|
292
|
-
body,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (eventType === "v2") {
|
|
297
|
-
return {
|
|
298
|
-
statusCode,
|
|
299
|
-
headers,
|
|
300
|
-
cookies,
|
|
301
|
-
// multiValueHeaders: {
|
|
302
|
-
// "Set-Cookie": cookies,
|
|
303
|
-
// },
|
|
304
|
-
isBase64Encoded,
|
|
305
|
-
body,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
statusCode,
|
|
311
|
-
headers,
|
|
312
|
-
multiValueHeaders: {
|
|
313
|
-
"Set-Cookie": cookies,
|
|
314
|
-
},
|
|
315
|
-
isBase64Encoded,
|
|
316
|
-
body,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async function sendResponse(response: unknown): Promise<void> {
|
|
321
|
-
if (requestId === undefined) {
|
|
322
|
-
exit("Runtime attempted to send a response without a request ID");
|
|
323
|
-
}
|
|
324
|
-
await fetch(`runtime/invocation/${requestId}/response`, {
|
|
325
|
-
method: "POST",
|
|
326
|
-
body:
|
|
327
|
-
response === null
|
|
328
|
-
? null
|
|
329
|
-
: typeof response === "string"
|
|
330
|
-
? response
|
|
331
|
-
: JSON.stringify(response),
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function formatBody(body?: string, isBase64Encoded?: boolean): string | null {
|
|
336
|
-
if (body === undefined) {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
if (!isBase64Encoded) {
|
|
340
|
-
return body;
|
|
341
|
-
}
|
|
342
|
-
return Buffer.from(body, "base64").toString("utf8");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
type HttpEventV1 = {
|
|
346
|
-
readonly requestContext: {
|
|
347
|
-
readonly requestId: string;
|
|
348
|
-
readonly domainName: string;
|
|
349
|
-
readonly httpMethod: string;
|
|
350
|
-
readonly path: string;
|
|
351
|
-
};
|
|
352
|
-
readonly headers: Record<string, string>;
|
|
353
|
-
readonly multiValueHeaders?: Record<string, string[]>;
|
|
354
|
-
readonly queryStringParameters?: Record<string, string>;
|
|
355
|
-
readonly multiValueQueryStringParameters?: Record<string, string[]>;
|
|
356
|
-
readonly isBase64Encoded: boolean;
|
|
357
|
-
readonly body?: string;
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
function isHttpEventV1(event: any): event is HttpEventV1 {
|
|
361
|
-
return (
|
|
362
|
-
!event.Records &&
|
|
363
|
-
event.version !== "2.0" &&
|
|
364
|
-
event.version !== "0" &&
|
|
365
|
-
typeof event.requestContext === "object"
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function formatHttpEventV1(event: HttpEventV1): Request {
|
|
370
|
-
const request = event.requestContext;
|
|
371
|
-
const headers = new Headers();
|
|
372
|
-
for (const [name, values] of Object.entries(event.multiValueHeaders ?? {})) {
|
|
373
|
-
for (const value of values) {
|
|
374
|
-
headers.append(name, value);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
const hostname = headers.get("Host") ?? request.domainName;
|
|
378
|
-
const proto = headers.get("X-Forwarded-Proto") ?? "http";
|
|
379
|
-
const url = new URL(request.path, `${proto}://${hostname}/`);
|
|
380
|
-
for (const [name, values] of Object.entries(
|
|
381
|
-
event.multiValueQueryStringParameters ?? {},
|
|
382
|
-
)) {
|
|
383
|
-
for (const value of values ?? []) {
|
|
384
|
-
url.searchParams.append(name, value);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (event.requestContext.authorizer != null) {
|
|
389
|
-
headers.set(
|
|
390
|
-
"x-amzn-authorizer",
|
|
391
|
-
JSON.stringify(event.requestContext.authorizer),
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return new Request(url.toString(), {
|
|
396
|
-
method: request.httpMethod,
|
|
397
|
-
headers,
|
|
398
|
-
body: formatBody(event.body, event.isBase64Encoded),
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
type HttpEventV2 = {
|
|
403
|
-
readonly version: "2.0";
|
|
404
|
-
readonly requestContext: {
|
|
405
|
-
readonly requestId: string;
|
|
406
|
-
readonly domainName: string;
|
|
407
|
-
readonly http: {
|
|
408
|
-
readonly method: string;
|
|
409
|
-
readonly path: string;
|
|
410
|
-
};
|
|
411
|
-
};
|
|
412
|
-
readonly headers: Record<string, string>;
|
|
413
|
-
readonly queryStringParameters?: Record<string, string>;
|
|
414
|
-
readonly cookies?: string[];
|
|
415
|
-
readonly isBase64Encoded: boolean;
|
|
416
|
-
readonly body?: string;
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
function isHttpEventV2(event: any): event is HttpEventV2 {
|
|
420
|
-
return (
|
|
421
|
-
!event.Records &&
|
|
422
|
-
event.version === "2.0" &&
|
|
423
|
-
typeof event.requestContext === "object"
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function formatHttpEventV2(event: HttpEventV2): Request {
|
|
428
|
-
const request = event.requestContext;
|
|
429
|
-
const headers = new Headers();
|
|
430
|
-
for (const [name, values] of Object.entries(event.headers)) {
|
|
431
|
-
for (const value of values.split(",")) {
|
|
432
|
-
headers.append(name, value);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
for (const cookie of event.cookies ?? []) {
|
|
436
|
-
headers.append("Set-Cookie", cookie);
|
|
437
|
-
}
|
|
438
|
-
const hostname = headers.get("Host") ?? request.domainName;
|
|
439
|
-
const proto = headers.get("X-Forwarded-Proto") ?? "http";
|
|
440
|
-
const url = new URL(request.http.path, `${proto}://${hostname}/`);
|
|
441
|
-
for (const [name, values] of Object.entries(
|
|
442
|
-
event.queryStringParameters ?? {},
|
|
443
|
-
)) {
|
|
444
|
-
url.searchParams.append(name, values);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (event.requestContext.authorizer != null) {
|
|
448
|
-
headers.set(
|
|
449
|
-
"x-amzn-authorizer",
|
|
450
|
-
JSON.stringify(event.requestContext.authorizer),
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return new Request(url.toString(), {
|
|
455
|
-
method: request.http.method,
|
|
456
|
-
headers,
|
|
457
|
-
body: formatBody(event.body, event.isBase64Encoded),
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function isHttpEvent(event: any): boolean {
|
|
462
|
-
return isHttpEventV1(event) || isHttpEventV2(event);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
type WebSocketEvent = {
|
|
466
|
-
readonly headers: Record<string, string>;
|
|
467
|
-
readonly multiValueHeaders: Record<string, string[]>;
|
|
468
|
-
readonly isBase64Encoded: boolean;
|
|
469
|
-
readonly body?: string;
|
|
470
|
-
readonly requestContext: {
|
|
471
|
-
readonly apiId: string;
|
|
472
|
-
readonly requestId: string;
|
|
473
|
-
readonly connectionId: string;
|
|
474
|
-
readonly domainName: string;
|
|
475
|
-
readonly stage: string;
|
|
476
|
-
readonly identity: {
|
|
477
|
-
readonly sourceIp: string;
|
|
478
|
-
};
|
|
479
|
-
} & (
|
|
480
|
-
| {
|
|
481
|
-
readonly eventType: "CONNECT";
|
|
482
|
-
}
|
|
483
|
-
| {
|
|
484
|
-
readonly eventType: "MESSAGE";
|
|
485
|
-
}
|
|
486
|
-
| {
|
|
487
|
-
readonly eventType: "DISCONNECT";
|
|
488
|
-
readonly disconnectStatusCode: number;
|
|
489
|
-
readonly disconnectReason: string;
|
|
490
|
-
}
|
|
491
|
-
);
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
function isWebSocketEvent(event: any): event is WebSocketEvent {
|
|
495
|
-
return (
|
|
496
|
-
typeof event.requestContext === "object" &&
|
|
497
|
-
typeof event.requestContext.connectionId === "string"
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function isWebSocketUpgrade(event: any): event is WebSocketEvent {
|
|
502
|
-
return (
|
|
503
|
-
isWebSocketEvent(event) && event.requestContext.eventType === "CONNECT"
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function formatWebSocketUpgrade(event: WebSocketEvent): Request {
|
|
508
|
-
const request = event.requestContext;
|
|
509
|
-
const headers = new Headers();
|
|
510
|
-
headers.set("Upgrade", "websocket");
|
|
511
|
-
headers.set("x-amzn-connection-id", request.connectionId);
|
|
512
|
-
for (const [name, values] of Object.entries(event.multiValueHeaders as any)) {
|
|
513
|
-
for (const value of (values as any) ?? []) {
|
|
514
|
-
headers.append(name, value);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
const hostname = headers.get("Host") ?? request.domainName;
|
|
518
|
-
const proto = headers.get("X-Forwarded-Proto") ?? "http";
|
|
519
|
-
const url = new URL(`${proto}://${hostname}/${request.stage}`);
|
|
520
|
-
return new Request(url.toString(), {
|
|
521
|
-
headers,
|
|
522
|
-
body: formatBody(event.body, event.isBase64Encoded),
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function formatUnknownEvent(event: unknown): Request {
|
|
527
|
-
return new Request("https://lambda/", {
|
|
528
|
-
method: "POST",
|
|
529
|
-
body: JSON.stringify(event),
|
|
530
|
-
headers: {
|
|
531
|
-
"Content-Type": "application/json;charset=utf-8",
|
|
532
|
-
},
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function formatRequest(input: LambdaRequest): Request | undefined {
|
|
537
|
-
const { event, requestId, traceId, functionArn, deadlineMs } = input;
|
|
538
|
-
let request: Request;
|
|
539
|
-
if (isHttpEventV2(event)) {
|
|
540
|
-
request = formatHttpEventV2(event);
|
|
541
|
-
} else if (isHttpEventV1(event)) {
|
|
542
|
-
request = formatHttpEventV1(event);
|
|
543
|
-
} else if (isWebSocketEvent(event)) {
|
|
544
|
-
if (!isWebSocketUpgrade(event)) {
|
|
545
|
-
return undefined;
|
|
546
|
-
}
|
|
547
|
-
request = formatWebSocketUpgrade(event);
|
|
548
|
-
} else {
|
|
549
|
-
request = formatUnknownEvent(input);
|
|
550
|
-
}
|
|
551
|
-
request.headers.set("x-amzn-requestid", requestId);
|
|
552
|
-
request.headers.set("x-amzn-trace-id", traceId);
|
|
553
|
-
request.headers.set("x-amzn-function-arn", functionArn);
|
|
554
|
-
if (deadlineMs !== null) {
|
|
555
|
-
request.headers.set("x-amzn-deadline-ms", `${deadlineMs}`);
|
|
556
|
-
}
|
|
557
|
-
// @ts-ignore: Attach the original event to the Request
|
|
558
|
-
request.aws = event;
|
|
559
|
-
return request;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
class LambdaServer implements Server {
|
|
563
|
-
#lambda: Lambda;
|
|
564
|
-
#webSockets: Map<string, LambdaWebSocket>;
|
|
565
|
-
#upgrade: Response | null;
|
|
566
|
-
pendingRequests: number;
|
|
567
|
-
pendingWebSockets: number;
|
|
568
|
-
port: number;
|
|
569
|
-
hostname: string;
|
|
570
|
-
development: boolean;
|
|
571
|
-
|
|
572
|
-
constructor(lambda: Lambda) {
|
|
573
|
-
this.#lambda = lambda;
|
|
574
|
-
this.#webSockets = new Map();
|
|
575
|
-
this.#upgrade = null;
|
|
576
|
-
this.pendingRequests = 0;
|
|
577
|
-
this.pendingWebSockets = 0;
|
|
578
|
-
this.port = 80;
|
|
579
|
-
this.hostname = "lambda";
|
|
580
|
-
this.development = false;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
async accept(request: LambdaRequest): Promise<unknown> {
|
|
584
|
-
const deadlineMs =
|
|
585
|
-
request.deadlineMs === null ? Date.now() + 60_000 : request.deadlineMs;
|
|
586
|
-
const durationMs = Math.max(1, deadlineMs - Date.now());
|
|
587
|
-
let response: unknown;
|
|
588
|
-
try {
|
|
589
|
-
response = await Promise.race([
|
|
590
|
-
new Promise<undefined>((resolve) => setTimeout(resolve, durationMs)),
|
|
591
|
-
this.#acceptRequest(request),
|
|
592
|
-
]);
|
|
593
|
-
} catch (cause) {
|
|
594
|
-
await sendError("RequestError", cause);
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
if (response === undefined) {
|
|
598
|
-
await sendError("TimeoutError", "Function timed out");
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
return response;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async #acceptRequest(event: LambdaRequest): Promise<unknown> {
|
|
605
|
-
const request = formatRequest(event);
|
|
606
|
-
let response: Response | undefined;
|
|
607
|
-
if (request === undefined) {
|
|
608
|
-
await this.#acceptWebSocket(event.event);
|
|
609
|
-
} else {
|
|
610
|
-
response = await this.fetch(request);
|
|
611
|
-
if (response.status === 101) {
|
|
612
|
-
await this.#acceptWebSocket(event.event);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (response === undefined) {
|
|
616
|
-
return {
|
|
617
|
-
statusCode: 200,
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
if (!isHttpEvent(event.event)) {
|
|
621
|
-
return response.text();
|
|
622
|
-
}
|
|
623
|
-
return formatResponse(response, isHttpEventV2(event.event) ? "v2" : "v1");
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
async #acceptWebSocket(event: WebSocketEvent): Promise<void> {
|
|
627
|
-
const request = event.requestContext;
|
|
628
|
-
const { connectionId, eventType } = request;
|
|
629
|
-
const webSocket = this.#webSockets.get(connectionId);
|
|
630
|
-
if (webSocket === undefined || this.#lambda.websocket === undefined) {
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
const { open, message, close } = this.#lambda.websocket;
|
|
634
|
-
switch (eventType) {
|
|
635
|
-
case "CONNECT": {
|
|
636
|
-
if (open) {
|
|
637
|
-
await open(webSocket);
|
|
638
|
-
}
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
case "MESSAGE": {
|
|
642
|
-
if (message) {
|
|
643
|
-
const body = formatBody(event.body, event.isBase64Encoded);
|
|
644
|
-
if (body !== null) {
|
|
645
|
-
await message(webSocket, body);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
case "DISCONNECT": {
|
|
651
|
-
try {
|
|
652
|
-
if (close) {
|
|
653
|
-
const { disconnectStatusCode: code, disconnectReason: reason } =
|
|
654
|
-
request;
|
|
655
|
-
await close(webSocket, code, reason);
|
|
656
|
-
}
|
|
657
|
-
} finally {
|
|
658
|
-
this.#webSockets.delete(connectionId);
|
|
659
|
-
this.pendingWebSockets--;
|
|
660
|
-
}
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
stop(): void {
|
|
667
|
-
exit("Runtime exited because Server.stop() was called");
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
reload(options: any): void {
|
|
671
|
-
this.#lambda = {
|
|
672
|
-
fetch: options.fetch ?? this.#lambda.fetch,
|
|
673
|
-
error: options.error ?? this.#lambda.error,
|
|
674
|
-
websocket: options.websocket ?? this.#lambda.websocket,
|
|
675
|
-
};
|
|
676
|
-
this.port =
|
|
677
|
-
typeof options.port === "number"
|
|
678
|
-
? options.port
|
|
679
|
-
: typeof options.port === "string"
|
|
680
|
-
? parseInt(options.port)
|
|
681
|
-
: this.port;
|
|
682
|
-
this.hostname = options.hostname ?? this.hostname;
|
|
683
|
-
this.development = options.development ?? this.development;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
async fetch(request: Request): Promise<Response> {
|
|
687
|
-
this.pendingRequests++;
|
|
688
|
-
try {
|
|
689
|
-
let response = await this.#lambda.fetch(request, this);
|
|
690
|
-
if (response instanceof Response) {
|
|
691
|
-
return response;
|
|
692
|
-
}
|
|
693
|
-
if (response === undefined && this.#upgrade !== null) {
|
|
694
|
-
return this.#upgrade;
|
|
695
|
-
}
|
|
696
|
-
throw new Error("fetch() did not return a Response");
|
|
697
|
-
} catch (cause) {
|
|
698
|
-
console.error(cause);
|
|
699
|
-
if (this.#lambda.error !== undefined) {
|
|
700
|
-
try {
|
|
701
|
-
return await this.#lambda.error(cause);
|
|
702
|
-
} catch (cause) {
|
|
703
|
-
console.error(cause);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return new Response(null, { status: 500 });
|
|
707
|
-
} finally {
|
|
708
|
-
this.pendingRequests--;
|
|
709
|
-
this.#upgrade = null;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
upgrade<T = undefined>(
|
|
714
|
-
request: Request,
|
|
715
|
-
options?: {
|
|
716
|
-
headers?: HeadersInit;
|
|
717
|
-
data?: T;
|
|
718
|
-
},
|
|
719
|
-
): boolean {
|
|
720
|
-
if (
|
|
721
|
-
request.method === "GET" &&
|
|
722
|
-
request.headers.get("Upgrade")?.toLowerCase() === "websocket"
|
|
723
|
-
) {
|
|
724
|
-
this.#upgrade = new Response(null, {
|
|
725
|
-
status: 101,
|
|
726
|
-
headers: options?.headers,
|
|
727
|
-
});
|
|
728
|
-
if ("aws" in request && isWebSocketUpgrade(request.aws)) {
|
|
729
|
-
const { connectionId } = request.aws.requestContext;
|
|
730
|
-
this.#webSockets.set(
|
|
731
|
-
connectionId,
|
|
732
|
-
new LambdaWebSocket(request.aws, options?.data),
|
|
733
|
-
);
|
|
734
|
-
this.pendingWebSockets++;
|
|
735
|
-
}
|
|
736
|
-
return true;
|
|
737
|
-
}
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
publish(
|
|
742
|
-
topic: string,
|
|
743
|
-
data: string | ArrayBuffer | ArrayBufferView,
|
|
744
|
-
compress?: boolean,
|
|
745
|
-
): number {
|
|
746
|
-
let count = 0;
|
|
747
|
-
for (const webSocket of this.#webSockets.values()) {
|
|
748
|
-
count += webSocket.publish(topic, data, compress) ? 1 : 0;
|
|
749
|
-
}
|
|
750
|
-
return count;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
class LambdaWebSocket implements ServerWebSocket {
|
|
755
|
-
#connectionId: string;
|
|
756
|
-
#url: string;
|
|
757
|
-
#invokeArn: string;
|
|
758
|
-
#topics: Set<string> | null;
|
|
759
|
-
remoteAddress: string;
|
|
760
|
-
readyState: 0 | 2 | 1 | -1 | 3;
|
|
761
|
-
binaryType?: "arraybuffer" | "uint8array";
|
|
762
|
-
data: any;
|
|
763
|
-
|
|
764
|
-
constructor(event: WebSocketEvent, data?: any) {
|
|
765
|
-
const request = event.requestContext;
|
|
766
|
-
this.#connectionId = `${request.connectionId}`;
|
|
767
|
-
this.#url = `https://${request.domainName}/${request.stage}/@connections/${this.#connectionId}`;
|
|
768
|
-
const [region, accountId] = (functionArn ?? "").split(":").slice(3, 5);
|
|
769
|
-
this.#invokeArn = `arn:aws:execute-api:${region}:${accountId}:${request.apiId}/${request.stage}/*`;
|
|
770
|
-
this.#topics = null;
|
|
771
|
-
this.remoteAddress = request.identity.sourceIp;
|
|
772
|
-
this.readyState = 1; // WebSocket.OPEN
|
|
773
|
-
this.data = data;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
send(
|
|
777
|
-
data: string | ArrayBuffer | ArrayBufferView,
|
|
778
|
-
compress?: boolean,
|
|
779
|
-
): number {
|
|
780
|
-
if (typeof data === "string") {
|
|
781
|
-
return this.sendText(data, compress);
|
|
782
|
-
}
|
|
783
|
-
if (data instanceof ArrayBuffer) {
|
|
784
|
-
return this.sendBinary(new Uint8Array(data), compress);
|
|
785
|
-
}
|
|
786
|
-
const buffer = new Uint8Array(
|
|
787
|
-
data.buffer,
|
|
788
|
-
data.byteOffset,
|
|
789
|
-
data.byteLength,
|
|
790
|
-
);
|
|
791
|
-
return this.sendBinary(buffer, compress);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
sendText(data: string, compress?: boolean): number {
|
|
795
|
-
fetchAws(this.#url, {
|
|
796
|
-
method: "POST",
|
|
797
|
-
body: data,
|
|
798
|
-
})
|
|
799
|
-
.then(({ status }) => {
|
|
800
|
-
if (status === 403) {
|
|
801
|
-
warnOnce(
|
|
802
|
-
"Failed to send WebSocket message due to insufficient IAM permissions",
|
|
803
|
-
`Assign the following IAM policy to ${functionArn} to fix this issue:`,
|
|
804
|
-
{
|
|
805
|
-
Version: "2012-10-17",
|
|
806
|
-
Statement: [
|
|
807
|
-
{
|
|
808
|
-
Effect: "Allow",
|
|
809
|
-
Action: ["execute-api:Invoke"],
|
|
810
|
-
Resource: [this.#invokeArn],
|
|
811
|
-
},
|
|
812
|
-
],
|
|
813
|
-
},
|
|
814
|
-
);
|
|
815
|
-
} else {
|
|
816
|
-
warnOnce(`Failed to send WebSocket message due to a ${status} error`);
|
|
817
|
-
}
|
|
818
|
-
})
|
|
819
|
-
.catch((error) => {
|
|
820
|
-
warnOnce("Failed to send WebSocket message", error);
|
|
821
|
-
});
|
|
822
|
-
return data.length;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
sendBinary(data: Uint8Array, compress?: boolean): number {
|
|
826
|
-
warnOnce(
|
|
827
|
-
"Lambda does not support binary WebSocket messages",
|
|
828
|
-
"https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-binary-media-types.html",
|
|
829
|
-
);
|
|
830
|
-
const base64 = Buffer.from(data).toString("base64");
|
|
831
|
-
return this.sendText(base64, compress);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
publish(
|
|
835
|
-
topic: string,
|
|
836
|
-
data: string | ArrayBuffer | ArrayBufferView,
|
|
837
|
-
compress?: boolean,
|
|
838
|
-
): number {
|
|
839
|
-
if (this.isSubscribed(topic)) {
|
|
840
|
-
return this.send(data, compress);
|
|
841
|
-
}
|
|
842
|
-
return -1;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
publishText(topic: string, data: string, compress?: boolean): number {
|
|
846
|
-
if (this.isSubscribed(topic)) {
|
|
847
|
-
return this.sendText(data, compress);
|
|
848
|
-
}
|
|
849
|
-
return -1;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
publishBinary(topic: string, data: Uint8Array, compress?: boolean): number {
|
|
853
|
-
if (this.isSubscribed(topic)) {
|
|
854
|
-
return this.sendBinary(data, compress);
|
|
855
|
-
}
|
|
856
|
-
return -1;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
close(code?: number, reason?: string): void {
|
|
860
|
-
// TODO: code? reason?
|
|
861
|
-
fetchAws(this.#url, {
|
|
862
|
-
method: "DELETE",
|
|
863
|
-
})
|
|
864
|
-
.then(({ status }) => {
|
|
865
|
-
if (status === 403) {
|
|
866
|
-
warnOnce(
|
|
867
|
-
"Failed to close WebSocket due to insufficient IAM permissions",
|
|
868
|
-
`Assign the following IAM policy to ${functionArn} to fix this issue:`,
|
|
869
|
-
{
|
|
870
|
-
Version: "2012-10-17",
|
|
871
|
-
Statement: [
|
|
872
|
-
{
|
|
873
|
-
Effect: "Allow",
|
|
874
|
-
Action: ["execute-api:Invoke"],
|
|
875
|
-
Resource: [this.#invokeArn],
|
|
876
|
-
},
|
|
877
|
-
],
|
|
878
|
-
},
|
|
879
|
-
);
|
|
880
|
-
} else {
|
|
881
|
-
warnOnce(`Failed to close WebSocket due to a ${status} error`);
|
|
882
|
-
}
|
|
883
|
-
})
|
|
884
|
-
.catch((error) => {
|
|
885
|
-
warnOnce("Failed to close WebSocket", error);
|
|
886
|
-
});
|
|
887
|
-
this.readyState = 3; // WebSocket.CLOSED;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
subscribe(topic: string): void {
|
|
891
|
-
if (this.#topics === null) {
|
|
892
|
-
this.#topics = new Set();
|
|
893
|
-
}
|
|
894
|
-
this.#topics.add(topic);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
unsubscribe(topic: string): void {
|
|
898
|
-
if (this.#topics !== null) {
|
|
899
|
-
this.#topics.delete(topic);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
isSubscribed(topic: string): boolean {
|
|
904
|
-
return this.#topics !== null && this.#topics.has(topic);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
cork(
|
|
908
|
-
callback: (ws: ServerWebSocket<undefined>) => any,
|
|
909
|
-
): void | Promise<void> {
|
|
910
|
-
// Lambda does not support sending multiple messages at a time.
|
|
911
|
-
return callback(this);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const lambda = await init();
|
|
916
|
-
const server = new LambdaServer(lambda);
|
|
917
|
-
while (true) {
|
|
918
|
-
try {
|
|
919
|
-
const request = await receiveRequest();
|
|
920
|
-
const response = await server.accept(request);
|
|
921
|
-
if (response !== undefined) {
|
|
922
|
-
await sendResponse(response);
|
|
923
|
-
}
|
|
924
|
-
} finally {
|
|
925
|
-
reset();
|
|
926
|
-
}
|
|
927
|
-
}
|