@ts-cloud/core 0.5.1 → 0.5.3
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/dist/bun-bootstrap +5 -0
- package/dist/index.js +78 -5
- package/dist/node-bootstrap +5 -0
- package/dist/runtime/adapter.ts +287 -0
- package/dist/runtime-assets/bootstrap +39 -0
- package/dist/runtime-assets/cli-runtime.php +148 -0
- package/dist/runtime-assets/fastcgi-client.php +118 -0
- package/dist/runtime-assets/octane-runtime.php +184 -0
- package/dist/runtime-assets/runtime.php +207 -0
- package/dist/runtime.mjs +95 -0
- package/dist/types.d.ts +87 -2
- package/package.json +2 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Minimal FastCGI client for the ts-cloud PHP Lambda runtime.
|
|
4
|
+
*
|
|
5
|
+
* Speaks just enough of the FastCGI protocol to send one request (params + stdin)
|
|
6
|
+
* to php-fpm over a unix socket and read the full stdout/stderr response. This is
|
|
7
|
+
* the bridge between the Lambda Runtime API loop and Laravel's public/index.php
|
|
8
|
+
* served by php-fpm.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
namespace TsCloud;
|
|
12
|
+
|
|
13
|
+
final class FastCgiClient
|
|
14
|
+
{
|
|
15
|
+
const FCGI_VERSION = 1;
|
|
16
|
+
const BEGIN_REQUEST = 1;
|
|
17
|
+
const END_REQUEST = 3;
|
|
18
|
+
const PARAMS = 4;
|
|
19
|
+
const STDIN = 5;
|
|
20
|
+
const STDOUT = 6;
|
|
21
|
+
const STDERR = 7;
|
|
22
|
+
const RESPONDER = 1;
|
|
23
|
+
|
|
24
|
+
private string $socketPath;
|
|
25
|
+
|
|
26
|
+
public function __construct(string $socketPath)
|
|
27
|
+
{
|
|
28
|
+
$this->socketPath = $socketPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param array<string,string> $params FastCGI params (CGI environment)
|
|
33
|
+
* @return array{stdout:string,stderr:string}
|
|
34
|
+
*/
|
|
35
|
+
public function request(array $params, string $stdin = ''): array
|
|
36
|
+
{
|
|
37
|
+
$conn = @stream_socket_client('unix://' . $this->socketPath, $errno, $errstr, 30);
|
|
38
|
+
if ($conn === false) {
|
|
39
|
+
throw new \RuntimeException("FastCGI connect failed: {$errstr} ({$errno})");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
$requestId = 1;
|
|
43
|
+
|
|
44
|
+
// BEGIN_REQUEST (role=RESPONDER, flags=0 -> close connection when done).
|
|
45
|
+
$beginBody = pack('nCxxxxx', self::RESPONDER, 0);
|
|
46
|
+
fwrite($conn, $this->record(self::BEGIN_REQUEST, $requestId, $beginBody));
|
|
47
|
+
|
|
48
|
+
// PARAMS (name-value pairs), then an empty PARAMS record to terminate.
|
|
49
|
+
$paramsBody = '';
|
|
50
|
+
foreach ($params as $name => $value) {
|
|
51
|
+
$paramsBody .= $this->nameValuePair((string) $name, (string) $value);
|
|
52
|
+
}
|
|
53
|
+
if ($paramsBody !== '') {
|
|
54
|
+
fwrite($conn, $this->record(self::PARAMS, $requestId, $paramsBody));
|
|
55
|
+
}
|
|
56
|
+
fwrite($conn, $this->record(self::PARAMS, $requestId, ''));
|
|
57
|
+
|
|
58
|
+
// STDIN (body) then an empty STDIN record to terminate.
|
|
59
|
+
if ($stdin !== '') {
|
|
60
|
+
foreach (str_split($stdin, 65535) as $chunk) {
|
|
61
|
+
fwrite($conn, $this->record(self::STDIN, $requestId, $chunk));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
fwrite($conn, $this->record(self::STDIN, $requestId, ''));
|
|
65
|
+
|
|
66
|
+
// Read the response records.
|
|
67
|
+
$stdout = '';
|
|
68
|
+
$stderr = '';
|
|
69
|
+
while (!feof($conn)) {
|
|
70
|
+
$header = fread($conn, 8);
|
|
71
|
+
if ($header === false || strlen($header) < 8) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
$h = unpack('Cversion/Ctype/nrequestId/ncontentLength/CpaddingLength/Creserved', $header);
|
|
75
|
+
$content = '';
|
|
76
|
+
$remaining = $h['contentLength'];
|
|
77
|
+
while ($remaining > 0) {
|
|
78
|
+
$buf = fread($conn, $remaining);
|
|
79
|
+
if ($buf === false || $buf === '') {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
$content .= $buf;
|
|
83
|
+
$remaining -= strlen($buf);
|
|
84
|
+
}
|
|
85
|
+
if ($h['paddingLength'] > 0) {
|
|
86
|
+
fread($conn, $h['paddingLength']);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if ($h['type'] === self::STDOUT) {
|
|
90
|
+
$stdout .= $content;
|
|
91
|
+
} elseif ($h['type'] === self::STDERR) {
|
|
92
|
+
$stderr .= $content;
|
|
93
|
+
} elseif ($h['type'] === self::END_REQUEST) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fclose($conn);
|
|
99
|
+
|
|
100
|
+
return ['stdout' => $stdout, 'stderr' => $stderr];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private function record(int $type, int $requestId, string $content): string
|
|
104
|
+
{
|
|
105
|
+
$length = strlen($content);
|
|
106
|
+
$header = pack('CCnnCx', self::FCGI_VERSION, $type, $requestId, $length, 0);
|
|
107
|
+
return $header . $content;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private function nameValuePair(string $name, string $value): string
|
|
111
|
+
{
|
|
112
|
+
$nlen = strlen($name);
|
|
113
|
+
$vlen = strlen($value);
|
|
114
|
+
$out = $nlen < 128 ? chr($nlen) : pack('N', $nlen | 0x80000000);
|
|
115
|
+
$out .= $vlen < 128 ? chr($vlen) : pack('N', $vlen | 0x80000000);
|
|
116
|
+
return $out . $name . $value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* ts-cloud PHP Lambda HTTP runtime — Octane / persistent mode.
|
|
4
|
+
*
|
|
5
|
+
* Boots the Laravel application ONCE per cold start and serves each invocation
|
|
6
|
+
* in-process through the HTTP kernel (no php-fpm, no FastCGI hop), then resets
|
|
7
|
+
* request-scoped state between invocations. Lower latency than the FPM bridge at
|
|
8
|
+
* the cost of requiring an Octane-safe app. Selected when TSCLOUD_OCTANE=1.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
$runtimeApi = getenv('AWS_LAMBDA_RUNTIME_API');
|
|
12
|
+
$taskRoot = getenv('LAMBDA_TASK_ROOT') ?: '/var/task';
|
|
13
|
+
|
|
14
|
+
require $taskRoot . '/vendor/autoload.php';
|
|
15
|
+
|
|
16
|
+
// Boot the application + HTTP kernel once.
|
|
17
|
+
$app = require $taskRoot . '/bootstrap/app.php';
|
|
18
|
+
$kernel = $app->make(\Illuminate\Contracts\Http\Kernel::class);
|
|
19
|
+
|
|
20
|
+
$maintenance = getenv('MAINTENANCE_MODE') === '1';
|
|
21
|
+
$bypassSecret = getenv('MAINTENANCE_BYPASS_SECRET') ?: '';
|
|
22
|
+
|
|
23
|
+
while (true) {
|
|
24
|
+
$ctx = nextInvocation($runtimeApi);
|
|
25
|
+
if ($ctx === null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
[$requestId, $event] = $ctx;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
$response = handle($event, $kernel, $maintenance, $bypassSecret);
|
|
32
|
+
postResponse($runtimeApi, $requestId, $response);
|
|
33
|
+
} catch (\Throwable $e) {
|
|
34
|
+
postError($runtimeApi, $requestId, $e);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handle(array $event, $kernel, bool $maintenance, string $bypassSecret): array
|
|
39
|
+
{
|
|
40
|
+
if (($event['warmer'] ?? false) === true) {
|
|
41
|
+
return ['statusCode' => 200, 'headers' => ['content-type' => 'text/plain'], 'body' => 'warm', 'isBase64Encoded' => false];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$http = $event['requestContext']['http'] ?? [];
|
|
45
|
+
$headers = $event['headers'] ?? [];
|
|
46
|
+
|
|
47
|
+
if ($maintenance) {
|
|
48
|
+
$bypass = $headers['x-maintenance-bypass'] ?? '';
|
|
49
|
+
if ($bypassSecret === '' || $bypass !== $bypassSecret) {
|
|
50
|
+
return ['statusCode' => 503, 'headers' => ['content-type' => 'text/plain', 'retry-after' => '120'], 'body' => 'Service temporarily unavailable (maintenance mode)', 'isBase64Encoded' => false];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$body = $event['body'] ?? '';
|
|
55
|
+
if (($event['isBase64Encoded'] ?? false) === true) {
|
|
56
|
+
$body = base64_decode($body);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
$method = $http['method'] ?? 'GET';
|
|
60
|
+
$rawPath = $event['rawPath'] ?? '/';
|
|
61
|
+
$rawQuery = $event['rawQueryString'] ?? '';
|
|
62
|
+
|
|
63
|
+
// Build server vars + an Illuminate request.
|
|
64
|
+
$server = [
|
|
65
|
+
'REQUEST_METHOD' => $method,
|
|
66
|
+
'REQUEST_URI' => $rawQuery !== '' ? $rawPath . '?' . $rawQuery : $rawPath,
|
|
67
|
+
'QUERY_STRING' => $rawQuery,
|
|
68
|
+
'SERVER_NAME' => $event['requestContext']['domainName'] ?? 'localhost',
|
|
69
|
+
'SERVER_PORT' => '443',
|
|
70
|
+
'HTTPS' => 'on',
|
|
71
|
+
'REMOTE_ADDR' => $http['sourceIp'] ?? '127.0.0.1',
|
|
72
|
+
];
|
|
73
|
+
foreach ($headers as $name => $value) {
|
|
74
|
+
$server['HTTP_' . strtoupper(str_replace('-', '_', $name))] = $value;
|
|
75
|
+
if (strtolower($name) === 'content-type') {
|
|
76
|
+
$server['CONTENT_TYPE'] = $value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
parse_str($rawQuery, $query);
|
|
81
|
+
$cookies = [];
|
|
82
|
+
foreach ($event['cookies'] ?? [] as $cookie) {
|
|
83
|
+
$parts = explode('=', $cookie, 2);
|
|
84
|
+
if (count($parts) === 2) {
|
|
85
|
+
$cookies[$parts[0]] = urldecode($parts[1]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
$request = new \Illuminate\Http\Request(
|
|
90
|
+
$query,
|
|
91
|
+
[],
|
|
92
|
+
[],
|
|
93
|
+
$cookies,
|
|
94
|
+
[],
|
|
95
|
+
$server,
|
|
96
|
+
$body
|
|
97
|
+
);
|
|
98
|
+
$request->setMethod($method);
|
|
99
|
+
|
|
100
|
+
$response = $kernel->handle($request);
|
|
101
|
+
$result = marshalResponse($response);
|
|
102
|
+
$kernel->terminate($request, $response);
|
|
103
|
+
|
|
104
|
+
return $result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function marshalResponse($response): array
|
|
108
|
+
{
|
|
109
|
+
$content = $response->getContent();
|
|
110
|
+
$headers = [];
|
|
111
|
+
$cookies = [];
|
|
112
|
+
foreach ($response->headers->allPreserveCase() as $name => $values) {
|
|
113
|
+
if (strtolower($name) === 'set-cookie') {
|
|
114
|
+
foreach ($values as $v) {
|
|
115
|
+
$cookies[] = $v;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
$headers[$name] = implode(', ', $values);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$contentType = $headers['Content-Type'] ?? ($headers['content-type'] ?? 'text/html');
|
|
123
|
+
$isText = (bool) preg_match('#^(text/|application/(json|xml|javascript|x-www-form-urlencoded)|image/svg)#i', $contentType);
|
|
124
|
+
|
|
125
|
+
$out = [
|
|
126
|
+
'statusCode' => $response->getStatusCode(),
|
|
127
|
+
'headers' => $headers,
|
|
128
|
+
'isBase64Encoded' => !$isText,
|
|
129
|
+
'body' => $isText ? $content : base64_encode($content),
|
|
130
|
+
];
|
|
131
|
+
if (!empty($cookies)) {
|
|
132
|
+
$out['cookies'] = $cookies;
|
|
133
|
+
}
|
|
134
|
+
return $out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @return array{0:string,1:array}|null
|
|
139
|
+
*/
|
|
140
|
+
function nextInvocation(string $api): ?array
|
|
141
|
+
{
|
|
142
|
+
$ch = curl_init("http://{$api}/2018-06-01/runtime/invocation/next");
|
|
143
|
+
$headers = [];
|
|
144
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
145
|
+
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($c, $h) use (&$headers) {
|
|
146
|
+
$parts = explode(':', $h, 2);
|
|
147
|
+
if (count($parts) === 2) {
|
|
148
|
+
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
|
|
149
|
+
}
|
|
150
|
+
return strlen($h);
|
|
151
|
+
});
|
|
152
|
+
$body = curl_exec($ch);
|
|
153
|
+
curl_close($ch);
|
|
154
|
+
if ($body === false) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
$requestId = $headers['lambda-runtime-aws-request-id'] ?? '';
|
|
158
|
+
$event = json_decode($body, true) ?: [];
|
|
159
|
+
return [$requestId, $event];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function postResponse(string $api, string $requestId, array $response): void
|
|
163
|
+
{
|
|
164
|
+
$ch = curl_init("http://{$api}/2018-06-01/runtime/invocation/{$requestId}/response");
|
|
165
|
+
curl_setopt($ch, CURLOPT_POST, true);
|
|
166
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
167
|
+
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($response));
|
|
168
|
+
curl_exec($ch);
|
|
169
|
+
curl_close($ch);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function postError(string $api, string $requestId, \Throwable $e): void
|
|
173
|
+
{
|
|
174
|
+
$ch = curl_init("http://{$api}/2018-06-01/runtime/invocation/{$requestId}/error");
|
|
175
|
+
curl_setopt($ch, CURLOPT_POST, true);
|
|
176
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
177
|
+
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
178
|
+
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|
179
|
+
'errorType' => get_class($e),
|
|
180
|
+
'errorMessage' => $e->getMessage(),
|
|
181
|
+
]));
|
|
182
|
+
curl_exec($ch);
|
|
183
|
+
curl_close($ch);
|
|
184
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* ts-cloud PHP Lambda HTTP runtime (FPM mode).
|
|
4
|
+
*
|
|
5
|
+
* Implements the AWS Lambda Runtime API loop for the HTTP function: long-poll for
|
|
6
|
+
* the next invocation (an API Gateway v2 payload-format-2.0 event), bridge it to
|
|
7
|
+
* php-fpm over FastCGI (running Laravel's public/index.php), and post the response
|
|
8
|
+
* back in API Gateway v2 response shape.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
require __DIR__ . '/fastcgi-client.php';
|
|
12
|
+
|
|
13
|
+
use TsCloud\FastCgiClient;
|
|
14
|
+
|
|
15
|
+
$runtimeApi = getenv('AWS_LAMBDA_RUNTIME_API');
|
|
16
|
+
$taskRoot = getenv('LAMBDA_TASK_ROOT') ?: '/var/task';
|
|
17
|
+
$docRoot = $taskRoot . '/public';
|
|
18
|
+
$socketPath = '/tmp/.tscloud-fpm.sock';
|
|
19
|
+
|
|
20
|
+
$fpm = new FastCgiClient($socketPath);
|
|
21
|
+
|
|
22
|
+
// Wait for php-fpm to create its socket (bounded).
|
|
23
|
+
for ($i = 0; $i < 50 && !file_exists($socketPath); $i++) {
|
|
24
|
+
usleep(100000); // 100ms
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$maintenance = getenv('MAINTENANCE_MODE') === '1';
|
|
28
|
+
$bypassSecret = getenv('MAINTENANCE_BYPASS_SECRET') ?: '';
|
|
29
|
+
|
|
30
|
+
while (true) {
|
|
31
|
+
// 1. Get the next invocation.
|
|
32
|
+
$ctx = nextInvocation($runtimeApi);
|
|
33
|
+
if ($ctx === null) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
[$requestId, $event] = $ctx;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
$response = handle($event, $fpm, $docRoot, $maintenance, $bypassSecret);
|
|
40
|
+
postResponse($runtimeApi, $requestId, $response);
|
|
41
|
+
} catch (\Throwable $e) {
|
|
42
|
+
postError($runtimeApi, $requestId, $e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @return array{0:string,1:array}|null
|
|
48
|
+
*/
|
|
49
|
+
function nextInvocation(string $api): ?array
|
|
50
|
+
{
|
|
51
|
+
$ch = curl_init("http://{$api}/2018-06-01/runtime/invocation/next");
|
|
52
|
+
$headers = [];
|
|
53
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
54
|
+
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($c, $h) use (&$headers) {
|
|
55
|
+
$parts = explode(':', $h, 2);
|
|
56
|
+
if (count($parts) === 2) {
|
|
57
|
+
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
|
|
58
|
+
}
|
|
59
|
+
return strlen($h);
|
|
60
|
+
});
|
|
61
|
+
$body = curl_exec($ch);
|
|
62
|
+
curl_close($ch);
|
|
63
|
+
if ($body === false) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
$requestId = $headers['lambda-runtime-aws-request-id'] ?? '';
|
|
67
|
+
$event = json_decode($body, true) ?: [];
|
|
68
|
+
return [$requestId, $event];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handle(array $event, FastCgiClient $fpm, string $docRoot, bool $maintenance, string $bypassSecret): array
|
|
72
|
+
{
|
|
73
|
+
// Warmer ping (scheduled keep-warm rule): keep the container alive, return fast.
|
|
74
|
+
if (($event['warmer'] ?? false) === true) {
|
|
75
|
+
return ['statusCode' => 200, 'headers' => ['content-type' => 'text/plain'], 'body' => 'warm', 'isBase64Encoded' => false];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$http = $event['requestContext']['http'] ?? [];
|
|
79
|
+
$method = $http['method'] ?? 'GET';
|
|
80
|
+
$rawPath = $event['rawPath'] ?? '/';
|
|
81
|
+
$rawQuery = $event['rawQueryString'] ?? '';
|
|
82
|
+
$headers = $event['headers'] ?? [];
|
|
83
|
+
$cookies = $event['cookies'] ?? [];
|
|
84
|
+
|
|
85
|
+
// Maintenance mode: 503 unless the bypass secret is presented.
|
|
86
|
+
if ($maintenance) {
|
|
87
|
+
$bypass = $headers['x-maintenance-bypass'] ?? '';
|
|
88
|
+
if ($bypassSecret === '' || $bypass !== $bypassSecret) {
|
|
89
|
+
return [
|
|
90
|
+
'statusCode' => 503,
|
|
91
|
+
'headers' => ['content-type' => 'text/plain', 'retry-after' => '120'],
|
|
92
|
+
'body' => 'Service temporarily unavailable (maintenance mode)',
|
|
93
|
+
'isBase64Encoded' => false,
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
$body = $event['body'] ?? '';
|
|
99
|
+
if (($event['isBase64Encoded'] ?? false) === true) {
|
|
100
|
+
$body = base64_decode($body);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
$params = [
|
|
104
|
+
'GATEWAY_INTERFACE' => 'CGI/1.1',
|
|
105
|
+
'REQUEST_METHOD' => $method,
|
|
106
|
+
'SCRIPT_FILENAME' => $docRoot . '/index.php',
|
|
107
|
+
'SCRIPT_NAME' => '/index.php',
|
|
108
|
+
'PATH_INFO' => $rawPath,
|
|
109
|
+
'REQUEST_URI' => $rawQuery !== '' ? $rawPath . '?' . $rawQuery : $rawPath,
|
|
110
|
+
'QUERY_STRING' => $rawQuery,
|
|
111
|
+
'DOCUMENT_ROOT' => $docRoot,
|
|
112
|
+
'SERVER_PROTOCOL' => $http['protocol'] ?? 'HTTP/1.1',
|
|
113
|
+
'SERVER_SOFTWARE' => 'ts-cloud-lambda',
|
|
114
|
+
'REMOTE_ADDR' => $http['sourceIp'] ?? '127.0.0.1',
|
|
115
|
+
'SERVER_NAME' => $event['requestContext']['domainName'] ?? 'localhost',
|
|
116
|
+
'SERVER_PORT' => '443',
|
|
117
|
+
'HTTPS' => 'on',
|
|
118
|
+
'CONTENT_LENGTH' => (string) strlen($body),
|
|
119
|
+
];
|
|
120
|
+
if (isset($headers['content-type'])) {
|
|
121
|
+
$params['CONTENT_TYPE'] = $headers['content-type'];
|
|
122
|
+
}
|
|
123
|
+
if (!empty($cookies)) {
|
|
124
|
+
$headers['cookie'] = implode('; ', $cookies);
|
|
125
|
+
}
|
|
126
|
+
foreach ($headers as $name => $value) {
|
|
127
|
+
$key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
|
|
128
|
+
$params[$key] = $value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
$result = $fpm->request($params, $body);
|
|
132
|
+
return parseFpmResponse($result['stdout']);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseFpmResponse(string $raw): array
|
|
136
|
+
{
|
|
137
|
+
// Split headers from body.
|
|
138
|
+
$pos = strpos($raw, "\r\n\r\n");
|
|
139
|
+
if ($pos === false) {
|
|
140
|
+
$pos = strpos($raw, "\n\n");
|
|
141
|
+
$headerBlock = $pos === false ? '' : substr($raw, 0, $pos);
|
|
142
|
+
$body = $pos === false ? $raw : substr($raw, $pos + 2);
|
|
143
|
+
} else {
|
|
144
|
+
$headerBlock = substr($raw, 0, $pos);
|
|
145
|
+
$body = substr($raw, $pos + 4);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$statusCode = 200;
|
|
149
|
+
$headers = [];
|
|
150
|
+
$cookies = [];
|
|
151
|
+
foreach (preg_split('/\r\n|\n/', $headerBlock) as $line) {
|
|
152
|
+
if (trim($line) === '') {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
$parts = explode(':', $line, 2);
|
|
156
|
+
if (count($parts) !== 2) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
$name = strtolower(trim($parts[0]));
|
|
160
|
+
$value = trim($parts[1]);
|
|
161
|
+
if ($name === 'status') {
|
|
162
|
+
$statusCode = (int) substr($value, 0, 3);
|
|
163
|
+
} elseif ($name === 'set-cookie') {
|
|
164
|
+
$cookies[] = $value;
|
|
165
|
+
} else {
|
|
166
|
+
$headers[$name] = $value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
$contentType = $headers['content-type'] ?? 'text/html';
|
|
171
|
+
$isText = (bool) preg_match('#^(text/|application/(json|xml|javascript|x-www-form-urlencoded)|image/svg)#i', $contentType);
|
|
172
|
+
|
|
173
|
+
$response = [
|
|
174
|
+
'statusCode' => $statusCode,
|
|
175
|
+
'headers' => $headers,
|
|
176
|
+
'isBase64Encoded' => !$isText,
|
|
177
|
+
'body' => $isText ? $body : base64_encode($body),
|
|
178
|
+
];
|
|
179
|
+
if (!empty($cookies)) {
|
|
180
|
+
$response['cookies'] = $cookies;
|
|
181
|
+
}
|
|
182
|
+
return $response;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function postResponse(string $api, string $requestId, array $response): void
|
|
186
|
+
{
|
|
187
|
+
$ch = curl_init("http://{$api}/2018-06-01/runtime/invocation/{$requestId}/response");
|
|
188
|
+
curl_setopt($ch, CURLOPT_POST, true);
|
|
189
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
190
|
+
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($response));
|
|
191
|
+
curl_exec($ch);
|
|
192
|
+
curl_close($ch);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function postError(string $api, string $requestId, \Throwable $e): void
|
|
196
|
+
{
|
|
197
|
+
$ch = curl_init("http://{$api}/2018-06-01/runtime/invocation/{$requestId}/error");
|
|
198
|
+
curl_setopt($ch, CURLOPT_POST, true);
|
|
199
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
200
|
+
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
201
|
+
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|
202
|
+
'errorType' => get_class($e),
|
|
203
|
+
'errorMessage' => $e->getMessage(),
|
|
204
|
+
]));
|
|
205
|
+
curl_exec($ch);
|
|
206
|
+
curl_close($ch);
|
|
207
|
+
}
|
package/dist/runtime.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-cloud JS Lambda custom-runtime loop (provided.al2023).
|
|
3
|
+
*
|
|
4
|
+
* Shared by both the Node and Bun custom runtimes — it is plain ESM that runs
|
|
5
|
+
* under either `node /opt/runtime.mjs` or `bun /opt/runtime.mjs`. It implements
|
|
6
|
+
* the AWS Lambda Runtime API event loop and delegates each invocation to the
|
|
7
|
+
* handler exported by the deployment artifact (the ts-cloud serverless adapter
|
|
8
|
+
* already bundled `http`/`queue`/`cli` into `index.mjs`).
|
|
9
|
+
*
|
|
10
|
+
* The function's configured Handler (e.g. `index.http`) arrives as `_HANDLER`,
|
|
11
|
+
* so the same artifact + layer serve all three functions; only `_HANDLER`
|
|
12
|
+
* differs per function.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const RUNTIME_API = process.env.AWS_LAMBDA_RUNTIME_API
|
|
16
|
+
const BASE = `http://${RUNTIME_API}/2018-06-01/runtime`
|
|
17
|
+
const TASK_ROOT = process.env.LAMBDA_TASK_ROOT ?? '/var/task'
|
|
18
|
+
const HANDLER = process.env._HANDLER ?? 'index.http'
|
|
19
|
+
|
|
20
|
+
async function resolveHandler() {
|
|
21
|
+
const lastDot = HANDLER.lastIndexOf('.')
|
|
22
|
+
const file = lastDot === -1 ? HANDLER : HANDLER.slice(0, lastDot)
|
|
23
|
+
const exportName = lastDot === -1 ? 'http' : HANDLER.slice(lastDot + 1)
|
|
24
|
+
|
|
25
|
+
let mod
|
|
26
|
+
for (const ext of ['.mjs', '.js', '.cjs', '']) {
|
|
27
|
+
try {
|
|
28
|
+
mod = await import(`${TASK_ROOT}/${file}${ext}`)
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// try the next extension
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!mod)
|
|
36
|
+
throw new Error(`Cannot load handler module "${file}" from ${TASK_ROOT}`)
|
|
37
|
+
|
|
38
|
+
const fn = mod[exportName] ?? mod.default?.[exportName] ?? mod.default
|
|
39
|
+
if (typeof fn !== 'function')
|
|
40
|
+
throw new TypeError(`Handler "${HANDLER}" did not resolve to a function`)
|
|
41
|
+
return fn
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function postInitError(err) {
|
|
45
|
+
await fetch(`${BASE}/init/error`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ errorType: err?.name ?? 'InitError', errorMessage: String(err?.message ?? err) }),
|
|
49
|
+
}).catch(() => {})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
let handler
|
|
54
|
+
try {
|
|
55
|
+
handler = await resolveHandler()
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
await postInitError(err)
|
|
59
|
+
process.exit(1)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The Runtime API event loop: long-poll for the next invocation, run the
|
|
64
|
+
// handler, post the response (or error), repeat.
|
|
65
|
+
for (;;) {
|
|
66
|
+
const next = await fetch(`${BASE}/invocation/next`)
|
|
67
|
+
const requestId = next.headers.get('lambda-runtime-aws-request-id')
|
|
68
|
+
if (!requestId) continue
|
|
69
|
+
|
|
70
|
+
let event
|
|
71
|
+
try {
|
|
72
|
+
event = await next.json()
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
event = {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await handler(event)
|
|
80
|
+
await fetch(`${BASE}/invocation/${requestId}/response`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: result === undefined ? '' : JSON.stringify(result),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
await fetch(`${BASE}/invocation/${requestId}/error`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ errorType: err?.name ?? 'Error', errorMessage: String(err?.message ?? err) }),
|
|
90
|
+
}).catch(() => {})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main()
|