@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.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/README.md +52 -1
- package/dist/config-schema.json +73 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -5
- package/src/build/canonical-target.js +436 -0
- package/src/build/entities-native.js +564 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/native-http-effects.js +871 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2109 -0
- package/src/compiler/assets-package-out-parity-proof.js +338 -0
- package/src/compiler/assets-provider-proof.js +499 -0
- package/src/compiler/config-provider-resolution.js +573 -0
- package/src/compiler/fastly-adapter.js +564 -0
- package/src/compiler/fastly-command-entry.js +390 -0
- package/src/compiler/fastly-config-secret-serve.js +323 -0
- package/src/compiler/fastly-hostcall-binding.js +598 -0
- package/src/compiler/fastly-readiness.js +477 -0
- package/src/compiler/index.js +40 -0
- package/src/compiler/lifecycle-parity-contract.js +70 -0
- package/src/compiler/lifecycle-parity-proof.js +260 -0
- package/src/compiler/route-handler-effect-proof.js +255 -0
- package/src/compiler/runtime-provider-kv.js +237 -0
- package/src/compiler/runtime-provider-shape.js +109 -0
- package/src/config-api.js +63 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +73 -0
- package/src/index.js +3 -0
- package/src/index.ts +118 -0
- package/src/javascript/assets-adapter.js +122 -0
- package/src/javascript/bindings-adapter.js +237 -0
- package/src/javascript/errors.js +26 -0
- package/src/javascript/fetch-adapter.js +93 -0
- package/src/javascript/grip-broadcast.js +261 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +26 -0
- package/src/javascript/package-effects.js +68 -0
- package/src/javascript/runtime-host.js +108 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +776 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +260 -0
- package/src/javascript/target.js +73 -0
- package/src/javascript/test-runtime.js +407 -0
- package/src/provider-contract.js +72 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +597 -0
- package/src/runtime/index.js +28 -0
- package/src/runtime/provider-primitives-contract.js +272 -0
- package/src/runtime/provider-primitives.js +82 -0
- package/src/runtime/route-handler-effects.js +507 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-http-effects-host.js +390 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +599 -0
- package/src/toolchain/config.js +128 -0
- package/src/toolchain/index.js +343 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const http = require('node:http');
|
|
5
|
+
const net = require('node:net');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const fastlyComputeRealityVersion = 'pulse.fastly-compute-reality.v3';
|
|
10
|
+
const maxCapturedLogBytes = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
class FastlyComputeRealityError extends Error {
|
|
13
|
+
constructor(code, message, detail = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'FastlyComputeRealityError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.detail = Object.freeze({ ...detail });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function executable(file) {
|
|
22
|
+
if (!file) return false;
|
|
23
|
+
try {
|
|
24
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
25
|
+
return fs.statSync(file).isFile();
|
|
26
|
+
} catch (_) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pathCandidates(binaryName = 'fastly', env = process.env) {
|
|
32
|
+
const delimiter = process.platform === 'win32' ? ';' : ':';
|
|
33
|
+
const extension = process.platform === 'win32' ? '.exe' : '';
|
|
34
|
+
return String(env.PATH || '')
|
|
35
|
+
.split(delimiter)
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map((entry) => path.join(entry, `${binaryName}${extension}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveFastlyCliBinary(options = {}) {
|
|
41
|
+
const env = options.env || process.env;
|
|
42
|
+
const candidates = [
|
|
43
|
+
options.binary,
|
|
44
|
+
env.PULSE_FASTLY_BIN,
|
|
45
|
+
...pathCandidates('fastly', env)
|
|
46
|
+
].filter(Boolean).map((entry) => path.resolve(String(entry)));
|
|
47
|
+
const found = candidates.find(executable);
|
|
48
|
+
if (!found) {
|
|
49
|
+
throw new FastlyComputeRealityError(
|
|
50
|
+
'PULSE_FASTLY_CLI_UNAVAILABLE',
|
|
51
|
+
'The Fastly CLI is required for the Fastly Compute reality gate.',
|
|
52
|
+
{
|
|
53
|
+
searched: Object.freeze([...new Set(candidates)]),
|
|
54
|
+
hint: 'Install the Fastly CLI with Homebrew, a supported package manager, or an official Fastly CLI release; then ensure `fastly` is on PATH. CI may set PULSE_FASTLY_BIN explicitly.'
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveViceroyBinary(options = {}) {
|
|
62
|
+
const env = options.env || process.env;
|
|
63
|
+
const candidates = [
|
|
64
|
+
options.viceroyBinary,
|
|
65
|
+
env.PULSE_VICEROY_BIN,
|
|
66
|
+
...pathCandidates('viceroy', env)
|
|
67
|
+
].filter(Boolean).map((entry) => path.resolve(String(entry)));
|
|
68
|
+
const found = candidates.find(executable);
|
|
69
|
+
if (!found) {
|
|
70
|
+
throw new FastlyComputeRealityError(
|
|
71
|
+
'PULSE_VICEROY_UNAVAILABLE',
|
|
72
|
+
'The selected Viceroy override is unavailable.',
|
|
73
|
+
{
|
|
74
|
+
searched: Object.freeze([...new Set(candidates)]),
|
|
75
|
+
hint: 'Point PULSE_VICEROY_BIN at an executable Viceroy binary, or unset it and provide the Fastly CLI through PULSE_FASTLY_BIN or PATH.'
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function inspectViceroy(options = {}) {
|
|
83
|
+
const binary = resolveViceroyBinary(options);
|
|
84
|
+
const attempts = [['--version'], ['version']];
|
|
85
|
+
const records = [];
|
|
86
|
+
let selected;
|
|
87
|
+
for (const args of attempts) {
|
|
88
|
+
const result = spawnSync(binary, args, {
|
|
89
|
+
cwd: options.cwd || process.cwd(),
|
|
90
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
timeout: Number(options.timeoutMs || 10000),
|
|
93
|
+
windowsHide: true
|
|
94
|
+
});
|
|
95
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
|
|
96
|
+
records.push(Object.freeze({ args: Object.freeze([...args]), status: result.status, output, message: result.error && result.error.message }));
|
|
97
|
+
if (!result.error && result.status === 0) {
|
|
98
|
+
selected = { args, output };
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!selected) {
|
|
103
|
+
throw new FastlyComputeRealityError(
|
|
104
|
+
'PULSE_VICEROY_INSPECTION_FAILED',
|
|
105
|
+
`Unable to inspect Viceroy at ${binary}.`,
|
|
106
|
+
{ binary, attempts: Object.freeze(records) }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const match = selected.output.match(/(?:viceroy(?:\s+version)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/i);
|
|
110
|
+
const env = options.env || process.env;
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
binary,
|
|
113
|
+
version: match ? match[1] : undefined,
|
|
114
|
+
output: selected.output,
|
|
115
|
+
args: Object.freeze([...selected.args]),
|
|
116
|
+
source: options.viceroyBinary ? 'explicit' : env.PULSE_VICEROY_BIN ? 'environment' : 'path'
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function inspectFastlyCli(options = {}) {
|
|
121
|
+
const binary = resolveFastlyCliBinary(options);
|
|
122
|
+
const attempts = [['version'], ['--version']];
|
|
123
|
+
const records = [];
|
|
124
|
+
let selected;
|
|
125
|
+
for (const args of attempts) {
|
|
126
|
+
const result = spawnSync(binary, args, {
|
|
127
|
+
cwd: options.cwd || process.cwd(),
|
|
128
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
129
|
+
encoding: 'utf8',
|
|
130
|
+
timeout: Number(options.timeoutMs || 10000),
|
|
131
|
+
windowsHide: true
|
|
132
|
+
});
|
|
133
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
|
|
134
|
+
const match = output.match(/(?:fastly(?:\s+cli)?(?:\s+version)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/i);
|
|
135
|
+
const timedOutAfterCompleteOutput = result.error && result.error.code === 'ETIMEDOUT' && Boolean(match);
|
|
136
|
+
records.push(Object.freeze({
|
|
137
|
+
args: Object.freeze([...args]),
|
|
138
|
+
status: result.status,
|
|
139
|
+
output,
|
|
140
|
+
message: result.error && result.error.message,
|
|
141
|
+
completeVersionOutput: Boolean(match)
|
|
142
|
+
}));
|
|
143
|
+
if ((!result.error && result.status === 0) || timedOutAfterCompleteOutput) {
|
|
144
|
+
selected = {
|
|
145
|
+
args,
|
|
146
|
+
output,
|
|
147
|
+
version: match && match[1],
|
|
148
|
+
termination: timedOutAfterCompleteOutput ? 'timeout-after-complete-version-output' : 'clean-exit'
|
|
149
|
+
};
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!selected) {
|
|
154
|
+
throw new FastlyComputeRealityError(
|
|
155
|
+
'PULSE_FASTLY_CLI_INSPECTION_FAILED',
|
|
156
|
+
`Unable to inspect the Fastly CLI at ${binary}.`,
|
|
157
|
+
{ binary, attempts: Object.freeze(records) }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
binary,
|
|
162
|
+
version: selected.version,
|
|
163
|
+
output: selected.output,
|
|
164
|
+
args: Object.freeze([...selected.args]),
|
|
165
|
+
termination: selected.termination,
|
|
166
|
+
source: options.binary ? 'explicit' : (options.env || process.env).PULSE_FASTLY_BIN ? 'environment' : 'path'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function inspectFastlyComputeLauncher(options = {}) {
|
|
171
|
+
const env = options.env || process.env;
|
|
172
|
+
const requestedKind = options.launcherKind;
|
|
173
|
+
if (
|
|
174
|
+
requestedKind !== undefined
|
|
175
|
+
&& requestedKind !== 'fastly-cli'
|
|
176
|
+
&& requestedKind !== 'viceroy-direct'
|
|
177
|
+
) {
|
|
178
|
+
throw new FastlyComputeRealityError(
|
|
179
|
+
'PULSE_FASTLY_LAUNCHER_INVALID',
|
|
180
|
+
`Unknown Fastly Compute reality launcher: ${String(requestedKind)}.`,
|
|
181
|
+
{ launcherKind: requestedKind }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const explicitFastly = Boolean(
|
|
186
|
+
options.inspection
|
|
187
|
+
|| options.binary
|
|
188
|
+
|| env.PULSE_FASTLY_BIN
|
|
189
|
+
);
|
|
190
|
+
const explicitViceroy = Boolean(
|
|
191
|
+
options.viceroyInspection
|
|
192
|
+
|| options.viceroyBinary
|
|
193
|
+
|| env.PULSE_VICEROY_BIN
|
|
194
|
+
);
|
|
195
|
+
const useDirectViceroy = requestedKind === 'viceroy-direct'
|
|
196
|
+
|| (!requestedKind && !explicitFastly && explicitViceroy);
|
|
197
|
+
|
|
198
|
+
if (useDirectViceroy) {
|
|
199
|
+
const inspection = options.viceroyInspection || inspectViceroy(options);
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
kind: 'viceroy-direct',
|
|
202
|
+
owner: 'pulse-test-harness',
|
|
203
|
+
inspection,
|
|
204
|
+
fastlyCliInspection: undefined,
|
|
205
|
+
viceroyInspection: inspection
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (requestedKind === 'fastly-cli' || explicitFastly) {
|
|
210
|
+
const inspection = options.inspection || inspectFastlyCli(options);
|
|
211
|
+
const viceroyRequested = Boolean(
|
|
212
|
+
options.viceroyInspection
|
|
213
|
+
|| options.viceroyBinary
|
|
214
|
+
|| env.PULSE_VICEROY_BIN
|
|
215
|
+
|| options.requireViceroy
|
|
216
|
+
);
|
|
217
|
+
const viceroyInspection = viceroyRequested
|
|
218
|
+
? (options.viceroyInspection || inspectViceroy(options))
|
|
219
|
+
: undefined;
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
kind: 'fastly-cli',
|
|
222
|
+
owner: 'fastly-cli',
|
|
223
|
+
inspection,
|
|
224
|
+
fastlyCliInspection: inspection,
|
|
225
|
+
viceroyInspection
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const inspection = inspectFastlyCli(options);
|
|
231
|
+
return Object.freeze({
|
|
232
|
+
kind: 'fastly-cli',
|
|
233
|
+
owner: 'fastly-cli',
|
|
234
|
+
inspection,
|
|
235
|
+
fastlyCliInspection: inspection,
|
|
236
|
+
viceroyInspection: undefined
|
|
237
|
+
});
|
|
238
|
+
} catch (fastlyError) {
|
|
239
|
+
if (fastlyError.code !== 'PULSE_FASTLY_CLI_UNAVAILABLE') throw fastlyError;
|
|
240
|
+
try {
|
|
241
|
+
const inspection = inspectViceroy(options);
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
kind: 'viceroy-direct',
|
|
244
|
+
owner: 'pulse-test-harness',
|
|
245
|
+
inspection,
|
|
246
|
+
fastlyCliInspection: undefined,
|
|
247
|
+
viceroyInspection: inspection
|
|
248
|
+
});
|
|
249
|
+
} catch (viceroyError) {
|
|
250
|
+
if (viceroyError.code !== 'PULSE_VICEROY_UNAVAILABLE') {
|
|
251
|
+
throw viceroyError;
|
|
252
|
+
}
|
|
253
|
+
throw new FastlyComputeRealityError(
|
|
254
|
+
'PULSE_FASTLY_REALITY_ENGINE_UNAVAILABLE',
|
|
255
|
+
'The Fastly Compute reality gate requires either the Fastly CLI or Viceroy.',
|
|
256
|
+
{
|
|
257
|
+
fastlyCli: fastlyError.detail,
|
|
258
|
+
viceroy: viceroyError.detail,
|
|
259
|
+
hint: 'Set PULSE_VICEROY_BIN to an executable Viceroy release, set PULSE_FASTLY_BIN to the Fastly CLI, or place either tool on PATH.'
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function tomlString(value) {
|
|
267
|
+
return JSON.stringify(String(value));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function tomlKey(value) {
|
|
271
|
+
return JSON.stringify(String(value));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function kvData(value) {
|
|
275
|
+
if (typeof value === 'string') return value;
|
|
276
|
+
return JSON.stringify({ __pulseKv: 1, value });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderFastlyLocalConfig(input = {}) {
|
|
280
|
+
const lines = [
|
|
281
|
+
'manifest_version = 3',
|
|
282
|
+
`name = ${tomlString(input.name || 'pulse-fastly-compute-reality')}`,
|
|
283
|
+
`description = ${tomlString(input.description || 'Pulse Fastly Compute reality fixture')}`,
|
|
284
|
+
'authors = []',
|
|
285
|
+
'language = "other"',
|
|
286
|
+
''
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
for (const [name, backend] of Object.entries(input.backends || {})) {
|
|
290
|
+
const value = typeof backend === 'string' ? { url: backend } : backend || {};
|
|
291
|
+
lines.push(`[local_server.backends.${tomlKey(name)}]`);
|
|
292
|
+
lines.push(`url = ${tomlString(value.url)}`);
|
|
293
|
+
if (value.overrideHost !== undefined) lines.push(`override_host = ${tomlString(value.overrideHost)}`);
|
|
294
|
+
if (value.useSni !== undefined) lines.push(`use_sni = ${value.useSni ? 'true' : 'false'}`);
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const [name, contents] of Object.entries(input.configStores || {})) {
|
|
299
|
+
lines.push(`[local_server.config_stores.${tomlKey(name)}]`);
|
|
300
|
+
lines.push('format = "inline-toml"');
|
|
301
|
+
lines.push(`[local_server.config_stores.${tomlKey(name)}.contents]`);
|
|
302
|
+
for (const [key, value] of Object.entries(contents || {})) lines.push(`${tomlKey(key)} = ${tomlString(value)}`);
|
|
303
|
+
lines.push('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const secretStores = Object.entries(input.secretStores || {});
|
|
307
|
+
if (secretStores.length > 0) {
|
|
308
|
+
lines.push('[local_server.secret_stores]');
|
|
309
|
+
for (const [name, contents] of secretStores) {
|
|
310
|
+
const entries = Object.entries(contents || {}).map(([key, value]) => `{ key = ${tomlString(key)}, data = ${tomlString(value)} }`);
|
|
311
|
+
lines.push(`${tomlKey(name)} = [${entries.join(', ')}]`);
|
|
312
|
+
}
|
|
313
|
+
lines.push('');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const kvStores = Object.entries(input.kvStores || {});
|
|
317
|
+
if (kvStores.length > 0) {
|
|
318
|
+
lines.push('[local_server.kv_stores]');
|
|
319
|
+
for (const [name, contents] of kvStores) {
|
|
320
|
+
const entries = Object.entries(contents || {}).map(([key, value]) => `{ key = ${tomlString(key)}, data = ${tomlString(kvData(value))} }`);
|
|
321
|
+
lines.push(`${tomlKey(name)} = [${entries.join(', ')}]`);
|
|
322
|
+
}
|
|
323
|
+
lines.push('');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return `${lines.join('\n').trim()}\n`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function reservePort(host = '127.0.0.1') {
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const server = net.createServer();
|
|
332
|
+
server.unref();
|
|
333
|
+
server.once('error', reject);
|
|
334
|
+
server.listen(0, host, () => {
|
|
335
|
+
const address = server.address();
|
|
336
|
+
const port = address && typeof address === 'object' ? address.port : undefined;
|
|
337
|
+
server.close((error) => error ? reject(error) : resolve(port));
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function waitForPort(host, port, child, logs, timeoutMs) {
|
|
343
|
+
const started = Date.now();
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
function attempt() {
|
|
346
|
+
if (child.exitCode !== null) {
|
|
347
|
+
reject(new FastlyComputeRealityError(
|
|
348
|
+
'PULSE_FASTLY_SERVE_START_FAILED',
|
|
349
|
+
`Fastly Compute serve exited before listening on ${host}:${port}.`,
|
|
350
|
+
{ exitCode: child.exitCode, signal: child.signalCode, stdout: logs.stdout, stderr: logs.stderr }
|
|
351
|
+
));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (Date.now() - started >= timeoutMs) {
|
|
355
|
+
reject(new FastlyComputeRealityError(
|
|
356
|
+
'PULSE_FASTLY_SERVE_START_TIMEOUT',
|
|
357
|
+
`Fastly Compute serve did not listen on ${host}:${port} within ${timeoutMs}ms.`,
|
|
358
|
+
{ stdout: logs.stdout, stderr: logs.stderr }
|
|
359
|
+
));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const socket = net.connect({ host, port });
|
|
363
|
+
socket.once('connect', () => {
|
|
364
|
+
socket.destroy();
|
|
365
|
+
resolve();
|
|
366
|
+
});
|
|
367
|
+
socket.once('error', () => {
|
|
368
|
+
socket.destroy();
|
|
369
|
+
setTimeout(attempt, 75);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
attempt();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function signalProcessTree(child, signal) {
|
|
377
|
+
if (!child || child.exitCode !== null || !child.pid) return;
|
|
378
|
+
if (process.platform === 'win32') {
|
|
379
|
+
const args = ['/PID', String(child.pid), '/T'];
|
|
380
|
+
if (signal === 'SIGKILL') args.push('/F');
|
|
381
|
+
spawnSync('taskkill', args, { stdio: 'ignore', windowsHide: true });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
process.kill(-child.pid, signal);
|
|
386
|
+
} catch (_) {
|
|
387
|
+
try { child.kill(signal); } catch (_) {}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function terminate(child, timeoutMs = 3000) {
|
|
392
|
+
if (!child || child.exitCode !== null) return Promise.resolve();
|
|
393
|
+
return new Promise((resolve) => {
|
|
394
|
+
let timer;
|
|
395
|
+
let fallback;
|
|
396
|
+
const done = () => {
|
|
397
|
+
if (timer) clearTimeout(timer);
|
|
398
|
+
if (fallback) clearTimeout(fallback);
|
|
399
|
+
resolve();
|
|
400
|
+
};
|
|
401
|
+
child.once('exit', done);
|
|
402
|
+
signalProcessTree(child, 'SIGTERM');
|
|
403
|
+
timer = setTimeout(() => {
|
|
404
|
+
if (child.exitCode === null) signalProcessTree(child, 'SIGKILL');
|
|
405
|
+
fallback = setTimeout(done, 150);
|
|
406
|
+
}, timeoutMs);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function appendLog(logs, key, chunk) {
|
|
411
|
+
const value = `${logs[key]}${chunk}`;
|
|
412
|
+
logs[key] = value.length <= maxCapturedLogBytes ? value : value.slice(value.length - maxCapturedLogBytes);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function startFastlyComputeServe(options = {}) {
|
|
416
|
+
const launcher = options.launcher || inspectFastlyComputeLauncher(options);
|
|
417
|
+
const inspection = launcher.inspection;
|
|
418
|
+
const fastlyCliInspection = launcher.fastlyCliInspection;
|
|
419
|
+
const viceroyInspection = launcher.viceroyInspection;
|
|
420
|
+
const packageRoot = path.resolve(options.packageRoot || options.cwd || process.cwd());
|
|
421
|
+
const wasmFile = path.resolve(options.wasmFile || path.join(packageRoot, 'bin', 'main.wasm'));
|
|
422
|
+
const manifestFile = path.resolve(options.manifestFile || path.join(packageRoot, 'fastly.toml'));
|
|
423
|
+
if (!fs.existsSync(wasmFile)) throw new FastlyComputeRealityError('PULSE_FASTLY_WASM_MISSING', `The native Fastly provider module is missing: ${wasmFile}`, { wasmFile });
|
|
424
|
+
if (!fs.existsSync(manifestFile)) throw new FastlyComputeRealityError('PULSE_FASTLY_MANIFEST_MISSING', `Fastly manifest is missing: ${manifestFile}`, { manifestFile });
|
|
425
|
+
if (path.dirname(manifestFile) !== packageRoot || path.basename(manifestFile) !== 'fastly.toml') {
|
|
426
|
+
throw new FastlyComputeRealityError(
|
|
427
|
+
'PULSE_FASTLY_MANIFEST_LOCATION_INVALID',
|
|
428
|
+
'Fastly Compute serve requires fastly.toml at the generated package root.',
|
|
429
|
+
{ packageRoot, manifestFile }
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const host = String(options.host || '127.0.0.1');
|
|
433
|
+
const port = Number(options.port || await reservePort(host));
|
|
434
|
+
const extraArgs = options.extraArgs === undefined ? [] : options.extraArgs;
|
|
435
|
+
if (!Array.isArray(extraArgs) || extraArgs.some((value) => typeof value !== 'string')) {
|
|
436
|
+
throw new FastlyComputeRealityError('PULSE_FASTLY_SERVE_ARGS_INVALID', 'Fastly Compute serve extraArgs must be an array of strings.', { extraArgs });
|
|
437
|
+
}
|
|
438
|
+
const args = launcher.kind === 'viceroy-direct'
|
|
439
|
+
? [
|
|
440
|
+
'serve',
|
|
441
|
+
'--addr', `${host}:${port}`,
|
|
442
|
+
'--config', manifestFile,
|
|
443
|
+
...extraArgs,
|
|
444
|
+
wasmFile
|
|
445
|
+
]
|
|
446
|
+
: [
|
|
447
|
+
'compute', 'serve',
|
|
448
|
+
'--dir', packageRoot,
|
|
449
|
+
'--file', wasmFile,
|
|
450
|
+
'--addr', `${host}:${port}`,
|
|
451
|
+
...(viceroyInspection ? ['--viceroy-path', viceroyInspection.binary] : []),
|
|
452
|
+
...extraArgs
|
|
453
|
+
];
|
|
454
|
+
const logs = { stdout: '', stderr: '' };
|
|
455
|
+
const child = spawn(inspection.binary, args, {
|
|
456
|
+
cwd: packageRoot,
|
|
457
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
458
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
459
|
+
windowsHide: true,
|
|
460
|
+
detached: process.platform !== 'win32'
|
|
461
|
+
});
|
|
462
|
+
child.stdout.setEncoding('utf8');
|
|
463
|
+
child.stderr.setEncoding('utf8');
|
|
464
|
+
child.stdout.on('data', (chunk) => appendLog(logs, 'stdout', chunk));
|
|
465
|
+
child.stderr.on('data', (chunk) => appendLog(logs, 'stderr', chunk));
|
|
466
|
+
try {
|
|
467
|
+
await waitForPort(host, port, child, logs, Number(options.startTimeoutMs || 45000));
|
|
468
|
+
} catch (error) {
|
|
469
|
+
await terminate(child, Number(options.stopTimeoutMs || 3000));
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
return Object.freeze({
|
|
473
|
+
version: fastlyComputeRealityVersion,
|
|
474
|
+
launcher,
|
|
475
|
+
inspection,
|
|
476
|
+
fastlyCliInspection,
|
|
477
|
+
viceroyInspection,
|
|
478
|
+
child,
|
|
479
|
+
host,
|
|
480
|
+
port,
|
|
481
|
+
baseUrl: `http://${host}:${port}`,
|
|
482
|
+
packageRoot,
|
|
483
|
+
wasmFile,
|
|
484
|
+
manifestFile,
|
|
485
|
+
args: Object.freeze([...args]),
|
|
486
|
+
logs,
|
|
487
|
+
stop: () => terminate(child, Number(options.stopTimeoutMs || 3000))
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function rawHeaders(pairs) {
|
|
492
|
+
const out = [];
|
|
493
|
+
for (let index = 0; index < pairs.length; index += 2) out.push([String(pairs[index]), String(pairs[index + 1])]);
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function requestFastlyCompute(server, input = {}) {
|
|
498
|
+
const body = input.body === undefined || input.body === null
|
|
499
|
+
? undefined
|
|
500
|
+
: Buffer.isBuffer(input.body) ? input.body : Buffer.from(String(input.body));
|
|
501
|
+
return new Promise((resolve, reject) => {
|
|
502
|
+
const request = http.request({
|
|
503
|
+
hostname: server.host,
|
|
504
|
+
port: server.port,
|
|
505
|
+
method: String(input.method || 'GET').toUpperCase(),
|
|
506
|
+
path: String(input.path || '/'),
|
|
507
|
+
headers: { ...(input.headers || {}), ...(body ? { 'content-length': String(body.length) } : {}) }
|
|
508
|
+
}, (response) => {
|
|
509
|
+
const chunks = [];
|
|
510
|
+
response.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
511
|
+
response.on('error', reject);
|
|
512
|
+
response.on('end', () => resolve(Object.freeze({
|
|
513
|
+
status: Number(response.statusCode || 0),
|
|
514
|
+
headers: Object.freeze(rawHeaders(response.rawHeaders || [])),
|
|
515
|
+
body: Buffer.concat(chunks)
|
|
516
|
+
})));
|
|
517
|
+
});
|
|
518
|
+
request.setTimeout(Number(input.timeoutMs || 30000), () => request.destroy(new FastlyComputeRealityError('PULSE_FASTLY_REQUEST_TIMEOUT', 'Fastly Compute request timed out.', { path: input.path })));
|
|
519
|
+
request.once('error', reject);
|
|
520
|
+
if (body) request.write(body);
|
|
521
|
+
request.end();
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
module.exports = Object.freeze({
|
|
526
|
+
fastlyComputeRealityVersion,
|
|
527
|
+
FastlyComputeRealityError,
|
|
528
|
+
resolveFastlyCliBinary,
|
|
529
|
+
inspectFastlyCli,
|
|
530
|
+
inspectFastlyComputeLauncher,
|
|
531
|
+
resolveViceroyBinary,
|
|
532
|
+
inspectViceroy,
|
|
533
|
+
renderFastlyLocalConfig,
|
|
534
|
+
reservePort,
|
|
535
|
+
startFastlyComputeServe,
|
|
536
|
+
requestFastlyCompute,
|
|
537
|
+
terminate
|
|
538
|
+
});
|