@pulse-compute/provider-fastly 1.0.0-beta.1 → 1.0.0-beta.11
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 +7 -6
- package/dist/config-schema.json +30 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
- package/src/build/canonical-target.js +41 -2
- package/src/build/digest-native.js +25 -0
- package/src/build/effect-invocations.js +51 -0
- package/src/build/embedded-assets.as.ts +32 -0
- package/src/build/entities-native.js +6 -11
- package/src/build/host-extensions.js +254 -0
- package/src/build/kv-native.as.ts +331 -0
- package/src/build/kv-native.js +27 -0
- package/src/build/lifecycle.js +50 -0
- package/src/build/native-application-errors.js +159 -0
- package/src/build/native-binary-resources.js +30 -0
- package/src/build/native-body-forwarding.as.ts +325 -0
- package/src/build/native-body-forwarding.js +73 -0
- package/src/build/native-dynamic-backends.as.ts +115 -0
- package/src/build/native-dynamic-backends.js +52 -0
- package/src/build/native-fetch-accounting.as.ts +133 -0
- package/src/build/native-fetch-accounting.js +84 -0
- package/src/build/native-fetch-bounds.js +47 -0
- package/src/build/native-fetch-cohorts.as.ts +126 -0
- package/src/build/native-fetch-cohorts.js +65 -0
- package/src/build/native-fetch-deadlines.as.ts +169 -0
- package/src/build/native-fetch-deadlines.js +36 -0
- package/src/build/native-fetch-errors.js +38 -0
- package/src/build/native-fetch-headers.js +52 -0
- package/src/build/native-fetch-outcomes.js +87 -0
- package/src/build/native-fetch-timing.as.ts +74 -0
- package/src/build/native-fetch-timing.js +50 -0
- package/src/build/native-forwarding-accounting.as.ts +153 -0
- package/src/build/native-forwarding-accounting.js +47 -0
- package/src/build/native-handle-ownership.js +127 -0
- package/src/build/native-header-lookup.js +50 -0
- package/src/build/native-http-effects.js +164 -35
- package/src/build/native-platform-capabilities.js +815 -220
- package/src/build/native-request-body.js +45 -0
- package/src/build/native-request-headers.js +93 -0
- package/src/build/native-send-error-detail.as.ts +12 -0
- package/src/build/native-stdio.js +45 -0
- package/src/build/native-string-values.js +143 -0
- package/src/build/native-transport-details.as.ts +84 -0
- package/src/build/native-value-budget.js +90 -0
- package/src/build/package-native-application.js +114 -0
- package/src/build/request-budget-source.js +58 -0
- package/src/build/request-budget.js +46 -0
- package/src/build/response-authority.js +56 -0
- package/src/build/s3-body.as.ts +97 -0
- package/src/build/s3-native.as.ts +228 -0
- package/src/build/s3-native.js +30 -0
- package/src/build/schema-admission.js +83 -0
- package/src/build/schema-nested-json.js +93 -0
- package/src/build/schema-scalar-record.js +41 -0
- package/src/build/time-native.js +49 -0
- package/src/build/wasm-budget.js +28 -0
- package/src/config-api.js +6 -0
- package/src/config-schema.json +30 -0
- package/src/index.ts +25 -0
- package/src/javascript/fetch-adapter.js +3 -0
- package/src/javascript/jwt-verifier.js +14 -4
- package/src/javascript/logging.js +11 -2
- package/src/javascript/package-effects.js +5 -2
- package/src/javascript/runtime-host.js +8 -3
- package/src/javascript/source-package.js +4 -2
- package/src/javascript/target-support-policy.js +20 -3
- package/src/javascript/target.js +17 -1
- package/src/javascript/test-runtime.js +1 -0
- package/src/provider-contract.js +23 -6
- package/src/runtime/canonical-api-runtime.js +6 -0
- package/src/testing/conditional-kv-host.js +155 -0
- package/src/testing/native-dynamic-backends-host.js +39 -0
- package/src/testing/native-http-effects-host.js +18 -1
- package/src/testing/native-platform-capabilities-host.js +359 -24
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +16 -0
- package/src/toolchain/index.js +49 -5
- package/src/toolchain/logging.js +18 -0
- package/src/toolchain/native-execution.js +83 -0
- package/src/toolchain/s3.js +26 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Provider-owned, request-local dynamic backend policy. No optional timeout or
|
|
2
|
+
// trust overrides: deadlines and certificate validation retain their owners.
|
|
3
|
+
@external("fastly_http_req", "register_dynamic_backend")
|
|
4
|
+
declare function __pulse_dynamic_register(name: usize, nameLength: i32, target: usize, targetLength: i32, mask: u32, config: usize): i32
|
|
5
|
+
const __pulse_dynamic_origins = new Array<string>()
|
|
6
|
+
const __pulse_dynamic_names = new Array<string>()
|
|
7
|
+
let __pulse_dynamic_attempts: i32 = 0
|
|
8
|
+
let __pulse_dynamic_status: i32 = 0
|
|
9
|
+
let __pulse_dynamic_url: string = ""
|
|
10
|
+
export function pulse_fastly_dynamic_backend_status(): i32 { return __pulse_dynamic_status }
|
|
11
|
+
function __pulse_dynamic_clear(): void {
|
|
12
|
+
__pulse_dynamic_origins.length = 0; __pulse_dynamic_names.length = 0; __pulse_dynamic_url = ""
|
|
13
|
+
}
|
|
14
|
+
function __pulse_dynamic_invalid(index: i32): string {
|
|
15
|
+
__pulse_fastly_fail(PULSE_ERROR_VALUE, 273, index); return ""
|
|
16
|
+
}
|
|
17
|
+
function __pulse_dynamic_select(index: i32, url: string): string {
|
|
18
|
+
__pulse_dynamic_url = url
|
|
19
|
+
const fixed = __pulse_fastly_backend(index)
|
|
20
|
+
if (fixed.length > 0) return fixed
|
|
21
|
+
if (!__pulse_dynamic_check(index)) return ""
|
|
22
|
+
if (url.length > PULSE_FASTLY_BUFFER_BYTES) return __pulse_dynamic_invalid(index)
|
|
23
|
+
for (let i = 0; i < url.length; i++) {
|
|
24
|
+
const c = url.charCodeAt(i)
|
|
25
|
+
if (c <= 32 || c >= 127 || c == 92) return __pulse_dynamic_invalid(index)
|
|
26
|
+
}
|
|
27
|
+
const separator = url.indexOf("://")
|
|
28
|
+
if (separator < 0) return __pulse_dynamic_invalid(index)
|
|
29
|
+
const scheme = url.substring(0, separator).toLowerCase()
|
|
30
|
+
const tls = scheme == "https"
|
|
31
|
+
if (!tls && scheme != "http") return __pulse_dynamic_invalid(index)
|
|
32
|
+
const start = separator + 3
|
|
33
|
+
let end = start
|
|
34
|
+
while (end < url.length && url.charAt(end) != "/" && url.charAt(end) != "?" && url.charAt(end) != "#") end++
|
|
35
|
+
const authority = url.substring(start, end).toLowerCase()
|
|
36
|
+
if (authority.length == 0 || authority.length > 1024 || authority.indexOf("@") >= 0) return __pulse_dynamic_invalid(index)
|
|
37
|
+
const colon = authority.indexOf(":")
|
|
38
|
+
let hostname = colon < 0 ? authority : authority.substring(0, colon)
|
|
39
|
+
let port = tls ? 443 : 80
|
|
40
|
+
if (colon >= 0) {
|
|
41
|
+
const digits = authority.substring(colon + 1)
|
|
42
|
+
if (digits.length == 0 || digits.length > 5) return __pulse_dynamic_invalid(index)
|
|
43
|
+
port = 0
|
|
44
|
+
for (let i = 0; i < digits.length; i++) {
|
|
45
|
+
const c = digits.charCodeAt(i)
|
|
46
|
+
if (c < 48 || c > 57) return __pulse_dynamic_invalid(index)
|
|
47
|
+
port = port * 10 + c - 48
|
|
48
|
+
}
|
|
49
|
+
if (port < 1 || port > 65535) return __pulse_dynamic_invalid(index)
|
|
50
|
+
}
|
|
51
|
+
const dns = hostname.endsWith(".") ? hostname.substring(0, hostname.length - 1) : hostname
|
|
52
|
+
if (dns.length == 0 || dns.length > 253) return __pulse_dynamic_invalid(index)
|
|
53
|
+
const labels = dns.split(".")
|
|
54
|
+
let allNumeric = true
|
|
55
|
+
for (let i = 0; i < labels.length; i++) {
|
|
56
|
+
const label = unchecked(labels[i])
|
|
57
|
+
if (label.length == 0 || label.length > 63 || label.startsWith("xn--") || label.startsWith("-") || label.endsWith("-")) return __pulse_dynamic_invalid(index)
|
|
58
|
+
for (let j = 0; j < label.length; j++) {
|
|
59
|
+
const c = label.charCodeAt(j)
|
|
60
|
+
if (!((c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c == 45)) return __pulse_dynamic_invalid(index)
|
|
61
|
+
if (c < 48 || c > 57) allNumeric = false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (allNumeric) {
|
|
65
|
+
if (tls || hostname != dns || labels.length != 4) return __pulse_dynamic_invalid(index)
|
|
66
|
+
for (let i = 0; i < 4; i++) {
|
|
67
|
+
const label = unchecked(labels[i])
|
|
68
|
+
if (label.length > 3 || (label.length > 1 && label.startsWith("0"))) return __pulse_dynamic_invalid(index)
|
|
69
|
+
let octet = 0
|
|
70
|
+
for (let j = 0; j < label.length; j++) octet = octet * 10 + label.charCodeAt(j) - 48
|
|
71
|
+
if (octet > 255) return __pulse_dynamic_invalid(index)
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// WHATWG numeric host aliases must not become a different DNS destination.
|
|
75
|
+
const last = unchecked(labels[labels.length - 1])
|
|
76
|
+
let numeric = true
|
|
77
|
+
for (let i = 0; i < last.length; i++) if (last.charCodeAt(i) < 48 || last.charCodeAt(i) > 57) numeric = false
|
|
78
|
+
if (numeric || last.startsWith("0x")) return __pulse_dynamic_invalid(index)
|
|
79
|
+
}
|
|
80
|
+
const host = hostname + (port == (tls ? 443 : 80) ? "" : ":" + port.toString())
|
|
81
|
+
const origin = scheme + "://" + host
|
|
82
|
+
let rest = url.substring(end)
|
|
83
|
+
const fragment = rest.indexOf("#")
|
|
84
|
+
if (fragment >= 0) rest = rest.substring(0, fragment)
|
|
85
|
+
if (!rest.startsWith("/")) rest = "/" + rest
|
|
86
|
+
__pulse_dynamic_url = origin + rest
|
|
87
|
+
if (__pulse_dynamic_url.length > PULSE_FASTLY_BUFFER_BYTES) return __pulse_dynamic_invalid(index)
|
|
88
|
+
const mapped = __pulse_dynamic_static(origin)
|
|
89
|
+
if (mapped.length > 0) return mapped
|
|
90
|
+
for (let i = 0; i < __pulse_dynamic_origins.length; i++) {
|
|
91
|
+
if (unchecked(__pulse_dynamic_origins[i]) == origin) return unchecked(__pulse_dynamic_names[i])
|
|
92
|
+
}
|
|
93
|
+
if (__pulse_dynamic_attempts >= 32) {
|
|
94
|
+
__pulse_fastly_fail(PULSE_ERROR_VALUE, 276, index); return ""
|
|
95
|
+
}
|
|
96
|
+
const name = "pulse-native-dynamic-" + (__pulse_dynamic_attempts++).toString()
|
|
97
|
+
const n = __pulse_fastly_utf8(name), target = __pulse_fastly_utf8(hostname + ":" + port.toString())
|
|
98
|
+
const h = __pulse_fastly_utf8(host), sni = __pulse_fastly_utf8(hostname)
|
|
99
|
+
const config = new StaticArray<u32>(27)
|
|
100
|
+
config[0] = <u32>changetype<usize>(h); config[1] = h.byteLength
|
|
101
|
+
let mask: u32 = 2
|
|
102
|
+
if (tls) {
|
|
103
|
+
mask |= 32 | 256 | 2048
|
|
104
|
+
config[7] = <u32>changetype<usize>(sni); config[8] = sni.byteLength
|
|
105
|
+
config[13] = <u32>changetype<usize>(sni); config[14] = sni.byteLength
|
|
106
|
+
}
|
|
107
|
+
if (!__pulse_dynamic_check(index)) return ""
|
|
108
|
+
__pulse_dynamic_status = __pulse_dynamic_register(changetype<usize>(n), n.byteLength, changetype<usize>(target), target.byteLength, mask, changetype<usize>(config))
|
|
109
|
+
if (!__pulse_dynamic_check(index)) return ""
|
|
110
|
+
if (__pulse_dynamic_status != 0) {
|
|
111
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, __pulse_dynamic_status == 5 ? 274 : 275, index); return ""
|
|
112
|
+
}
|
|
113
|
+
__pulse_dynamic_origins.push(origin); __pulse_dynamic_names.push(name)
|
|
114
|
+
return name
|
|
115
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const policy = Object.freeze({
|
|
6
|
+
optIn: 'bindings.dynamicBackends', maxRegistrationAttempts: 32,
|
|
7
|
+
identity: 'canonical origin with derived Host/TLS policy; request-local',
|
|
8
|
+
precedence: 'effect backend, configured canonical origin, dynamic registration',
|
|
9
|
+
urls: 'absolute ASCII HTTP/HTTPS DNS; canonical IPv4 over HTTP; ports 1–65535; no credentials, IDNA or IPv6',
|
|
10
|
+
tls: 'platform trust; URL DNS hostname for certificate verification and SNI',
|
|
11
|
+
hostPrerequisite: 'Fastly Dynamic Backends service capability',
|
|
12
|
+
stages: Object.freeze({ invalidUrl: 273, unavailable: 274, registrationFailed: 275, attemptsExhausted: 276 })
|
|
13
|
+
});
|
|
14
|
+
function active(bindings) { return (Array.isArray(bindings) ? bindings : bindings.backends || []).some(b => b.dynamic === true); }
|
|
15
|
+
function originMappings(origins) {
|
|
16
|
+
return Object.fromEntries(Object.entries(origins || {}).filter(([, name]) => typeof name === 'string' && name.trim()).map(([origin, name]) => [origin, name.trim()]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
17
|
+
}
|
|
18
|
+
function describe(origins) { return Object.freeze({ ...policy, staticOrigins: Object.freeze(originMappings(origins)) }); }
|
|
19
|
+
function runtimeSource(backends, platform, origins) {
|
|
20
|
+
if (!active(backends)) return '';
|
|
21
|
+
const mappings = Object.entries(originMappings(origins));
|
|
22
|
+
return fs.readFileSync(path.join(__dirname, 'native-dynamic-backends.as.ts'), 'utf8') + '\n'
|
|
23
|
+
+ `function __pulse_dynamic_static(origin: string): string {\n${mappings.map(([origin, name]) => ` if (origin == ${JSON.stringify(origin)}) return ${JSON.stringify(name.trim())}`).join('\n')}\n return ""\n}\n`
|
|
24
|
+
+ `function __pulse_dynamic_check(index: i32): bool { return ${platform ? '__pulse_fetch_request_check(index) && __pulse_fetch_before_send(index)' : 'true'} }\n`;
|
|
25
|
+
}
|
|
26
|
+
function instrument(source, bindings, platform, origins) {
|
|
27
|
+
if (!active(bindings)) return source;
|
|
28
|
+
const replace = (before, after) => {
|
|
29
|
+
if (!source.includes(before)) throw new Error('Fastly dynamic backend boundary drift: ' + before.slice(0, 100));
|
|
30
|
+
source = source.replace(before, after);
|
|
31
|
+
};
|
|
32
|
+
if (platform) {
|
|
33
|
+
replace('__pulse_fastly_start_request(effectIndex, url, method, headers, body, __pulse_fastly_backend(effectIndex))',
|
|
34
|
+
'const backend = __pulse_dynamic_select(effectIndex, url); if (__pulse_fastly_last_error != 0) return; __pulse_fastly_start_request(effectIndex, __pulse_dynamic_url, method, headers, body, backend)');
|
|
35
|
+
} else {
|
|
36
|
+
replace('function host_effect_begin(effectIndex: i32, payload: i32): void {',
|
|
37
|
+
'function host_effect_begin(effectIndex: i32, payload: i32): void { if (__pulse_fastly_last_error != 0) return;');
|
|
38
|
+
replace(' const requestOut = __pulse_fastly_out_i32(); let status = fastly_http_req_new',
|
|
39
|
+
' const backend = __pulse_dynamic_select(effectIndex, url); if (__pulse_fastly_last_error != 0) return;\n const requestOut = __pulse_fastly_out_i32(); let status = fastly_http_req_new');
|
|
40
|
+
replace('const urlBytes = __pulse_fastly_utf8(url); status = fastly_http_req_uri_set',
|
|
41
|
+
'const urlBytes = __pulse_fastly_utf8(__pulse_dynamic_url); status = fastly_http_req_uri_set');
|
|
42
|
+
replace('const backend = __pulse_fastly_backend(effectIndex); if (backend.length == 0)', 'if (backend.length == 0)');
|
|
43
|
+
}
|
|
44
|
+
const entry = source.includes('export function pulse_fastly_handle(): void {') ? 'pulse_fastly_handle' : '_start';
|
|
45
|
+
replace(`export function ${entry}(): void {`, 'function __pulse_dynamic_start(): void {');
|
|
46
|
+
return source + '\n' + runtimeSource(bindings, platform, origins) + `\nexport function ${entry}(): void {
|
|
47
|
+
__pulse_dynamic_clear(); __pulse_dynamic_attempts = 0; __pulse_dynamic_status = 0
|
|
48
|
+
__pulse_dynamic_start()
|
|
49
|
+
__pulse_dynamic_clear()
|
|
50
|
+
}\n`;
|
|
51
|
+
}
|
|
52
|
+
module.exports = { active, policy, describe, instrument };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Fixed one-cohort adapter. The existing effect owner and scheduler retain all
|
|
2
|
+
// dispatch/completion authority. Claims and retained text are never recycled.
|
|
3
|
+
let __fetch_ra: PulseResourceLedger | null = null;
|
|
4
|
+
let __fca_origin:i64 = 0;
|
|
5
|
+
let __fca_clock_failed:bool = false;
|
|
6
|
+
let __fca_reaped:bool = false;
|
|
7
|
+
const __fca_io:usize = memory.data(32);
|
|
8
|
+
const __fca_exit:usize = memory.data(64);
|
|
9
|
+
function __fca_ns(slot:i32):u32 { return slot == 0 ? 1 : slot == 2 ? 4 : slot == 3 || slot == 5 ? 2 : 3; }
|
|
10
|
+
function __fca_now():u64 {
|
|
11
|
+
if (__fca_clock_failed) return PRL_SAFE + 1;
|
|
12
|
+
const now = __request_clock();
|
|
13
|
+
if (now < __fca_origin || __request_expired) { __fca_clock_failed = true; return PRL_SAFE + 1; }
|
|
14
|
+
if (now >= __request_deadline) __request_expired = true;
|
|
15
|
+
return u64(now - __fca_origin);
|
|
16
|
+
}
|
|
17
|
+
function __fca_work():bool { return __fca_ok(__fca_now() < __FCA_DURATION); }
|
|
18
|
+
function __fca_ok(ok:bool):bool {
|
|
19
|
+
if (!ok) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL,272,0);
|
|
20
|
+
return ok;
|
|
21
|
+
}
|
|
22
|
+
function __fca_start():bool {
|
|
23
|
+
if (__fetch_ra !== null || __request_deadline < 0 || __request_last_clock < 0
|
|
24
|
+
|| __pulse_invocation_count != 1 || __pulse_invocation_tickets[0] != 1) return __fca_ok(false);
|
|
25
|
+
__fca_origin = __request_deadline - i64(__FCA_DURATION);
|
|
26
|
+
const l = new PulseResourceLedger(1,__FCA_RECORDS,7,__FCA_BACKING,7,1,__FCA_DURATION,__FCA_STORAGE);
|
|
27
|
+
__fetch_ra = l;
|
|
28
|
+
if (!l.begin(1,__fca_now(),5,2,1,true)) return __fca_ok(false);
|
|
29
|
+
for (let i=0;i<7;i++) if (!l.stageRecord(1,1,__fca_ns(i),1,i>=5?3:1,0)) return __fca_ok(false);
|
|
30
|
+
if (!l.stageRecord(1,2,0,1,1,100) || !l.stageRecord(1,2,0,1,1,__FCA_ENCODING)
|
|
31
|
+
|| !l.stageRecord(1,2,0,1,1,__FCA_READER) || !l.stageRecord(1,2,0,1,3,64)
|
|
32
|
+
|| !l.stageRecord(1,3,0,1,1,0)
|
|
33
|
+
/*FCA_HEADER_STAGE*/ || !l.commit(1,__fca_now())) return __fca_ok(false);
|
|
34
|
+
for (let i=0;i<__FCA_RECORDS;i++) if (l.reservedSlot(i)!=i || l.reservedGeneration(i)!=1) return __fca_ok(false);
|
|
35
|
+
return __fca_ok(l.consume(1,7,1,1,100,false,__fca_now())
|
|
36
|
+
&& l.consume(1,8,1,1,__FCA_ENCODING,false,__fca_now())
|
|
37
|
+
&& l.consume(1,9,1,1,__FCA_READER,false,__fca_now())
|
|
38
|
+
/*FCA_HEADER_CONSUME*/);
|
|
39
|
+
}
|
|
40
|
+
function __fca_acquire(slot:i32):bool {
|
|
41
|
+
return __fetch_ra !== null && __fca_ok(__fetch_ra!.beginAcquire(1,slot,1,__fca_ns(slot),1,slot>=5,__fca_now()));
|
|
42
|
+
}
|
|
43
|
+
function __fca_acquired(slot:i32,status:i32,handle:i32):i32 {
|
|
44
|
+
const l=__fetch_ra!;
|
|
45
|
+
if(status==0) __fca_ok(l.acquired(1,slot,1,__fca_ns(slot),1,u32(handle)));
|
|
46
|
+
else l.noAcquisition(1,slot,1,__fca_ns(slot),1,false);
|
|
47
|
+
return status;
|
|
48
|
+
}
|
|
49
|
+
function __fca_new(slot:i32,out:usize):i32 {
|
|
50
|
+
if(!__fca_acquire(slot)) return 1;
|
|
51
|
+
const ns=__fca_ns(slot);
|
|
52
|
+
const status=ns==1?fastly_http_req_new(out):ns==2?fastly_http_resp_new(out):fastly_http_body_new(out);
|
|
53
|
+
return __fca_acquired(slot,status,load<i32>(out));
|
|
54
|
+
}
|
|
55
|
+
function __fca_dispose(slot:i32):i32 {
|
|
56
|
+
if(__fetch_ra===null) return 1;
|
|
57
|
+
const l=__fetch_ra!,ns=__fca_ns(slot);
|
|
58
|
+
if(l.bank[slot*8+5]!=PRL_LIVE || !l.beginDispose(1,slot,1,ns,1,__fca_now())) return 1;
|
|
59
|
+
const handle=i32(l.bank[slot*8+6]);
|
|
60
|
+
const status=ns==1?__pulse_fetch_close_request(handle):ns==2?__pulse_fetch_close_response(handle):__pulse_fetch_close_body(handle);
|
|
61
|
+
l.disposed(1,slot,1,ns,1,status==0); return status;
|
|
62
|
+
}
|
|
63
|
+
function __fca_dispatch(request:i32,body:i32,backend:usize,length:i32,out:usize):i32 {
|
|
64
|
+
if(!__fca_acquire(2)||!__fca_acquire(3)||!__fca_acquire(4)) return 1;
|
|
65
|
+
const l=__fetch_ra!;
|
|
66
|
+
if(!l.beginDispose(1,0,1,1,1,__fca_now())||!l.beginDispose(1,1,1,3,1,__fca_now())) return 1;
|
|
67
|
+
const status=fastly_http_req_send_async(request,body,backend,length,out);
|
|
68
|
+
l.disposed(1,0,1,1,1,status==0); l.disposed(1,1,1,3,1,status==0);
|
|
69
|
+
return __fca_acquired(2,status,load<i32>(out));
|
|
70
|
+
}
|
|
71
|
+
function __fca_poll(handle:i32,detail:usize,done:usize,response:usize,body:usize):i32 {
|
|
72
|
+
const l=__fetch_ra!;
|
|
73
|
+
if(l.bank[2*8+5]!=PRL_LIVE || __fca_now()>=__FCA_DURATION) return 1;
|
|
74
|
+
const status=__pulse_fetch_poll(handle,detail,done,response,body), ready=load<i32>(done);
|
|
75
|
+
if(status!=0 || (ready!=0 && ready!=1)) {
|
|
76
|
+
l.uncertain(1,2,1,4,1); l.noAcquisition(1,3,1,2,1,false); l.noAcquisition(1,4,1,3,1,false);
|
|
77
|
+
return status==0?2:status;
|
|
78
|
+
}
|
|
79
|
+
if(ready==1) {
|
|
80
|
+
l.transferred(1,2,1,4,1);
|
|
81
|
+
__fca_acquired(3,0,load<i32>(response)); __fca_acquired(4,0,load<i32>(body));
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
function __fca_cleanup(final:bool=false):void {
|
|
86
|
+
if(__fetch_ra===null) return;
|
|
87
|
+
const l=__fetch_ra!;
|
|
88
|
+
l.close(1);
|
|
89
|
+
if(!__fca_reaped && l.bank[2*8+5]==PRL_LIVE) {
|
|
90
|
+
__fca_reaped=true;
|
|
91
|
+
if(l.cleanupStep(1,false,__fca_now())) {
|
|
92
|
+
__pulse_send_error_detail_reset(__fca_io); store<i32>(__fca_io+20,0);
|
|
93
|
+
__fca_poll(i32(l.bank[2*8+6]),__fca_io,__fca_io+20,__fca_io+24,__fca_io+28);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for(let i=0;i<7;i++) {
|
|
97
|
+
const state=l.bank[i*8+5];
|
|
98
|
+
if(state==PRL_RESERVED && (i<5||final)) l.unused(1,i,1,__fca_ns(i),1);
|
|
99
|
+
else if(state==PRL_LIVE && i!=2) __fca_dispose(i);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function __fca_error():bool {
|
|
103
|
+
if(__fetch_ra===null) return false;
|
|
104
|
+
const l=__fetch_ra!;
|
|
105
|
+
// Capture the primary driver failure before cleanup or protected delivery.
|
|
106
|
+
const status=__request_expired || __pulse_fastly_error_stage==271 ? 504
|
|
107
|
+
: __pulse_fastly_last_error==PULSE_ERROR_TRANSPORT ? 502 : 500;
|
|
108
|
+
if(l.primary==0) l.fail(1,u32(status));
|
|
109
|
+
__fca_cleanup();
|
|
110
|
+
if(!__pulse_response_owned() || !l.consume(1,10,1,1,64,true,__fca_now())) return true;
|
|
111
|
+
if(__fca_new(5,__fca_io+24)!=0) return true;
|
|
112
|
+
const response=load<i32>(__fca_io+24);
|
|
113
|
+
if(__fca_new(6,__fca_io+28)!=0) return true;
|
|
114
|
+
const body=load<i32>(__fca_io+28);
|
|
115
|
+
if(__fca_now()>=__FCA_DURATION || fastly_http_resp_status_set(response,status)!=0) return true;
|
|
116
|
+
const header='content-typetext/plain; charset=utf-8';
|
|
117
|
+
for(let i=0;i<header.length;i++) store<u8>(__fca_exit+i,u8(header.charCodeAt(i)));
|
|
118
|
+
if(__fca_now()>=__FCA_DURATION || fastly_http_resp_header_append(response,__fca_exit,12,__fca_exit+12,header.length-12)!=0) return true;
|
|
119
|
+
const text=status==504?'Gateway Timeout':status==502?'Bad Gateway':'Internal Server Error';
|
|
120
|
+
for(let i=0;i<text.length;i++) store<u8>(__fca_exit+i,u8(text.charCodeAt(i)));
|
|
121
|
+
let offset=0;
|
|
122
|
+
while(offset<text.length) {
|
|
123
|
+
if(__fca_now()>=__FCA_DURATION) return true;
|
|
124
|
+
const result=fastly_http_body_write(body,__fca_exit+offset,text.length-offset,0,__fca_io+20);
|
|
125
|
+
const written=load<i32>(__fca_io+20);
|
|
126
|
+
if(result!=0 || written<=0 || written>text.length-offset) return true;
|
|
127
|
+
offset+=written;
|
|
128
|
+
}
|
|
129
|
+
if(!l.beginDispose(1,5,1,2,1,__fca_now()) || !l.beginDispose(1,6,1,3,1,__fca_now())) return true;
|
|
130
|
+
const sent=fastly_http_resp_send_downstream(response,body,0);
|
|
131
|
+
l.disposed(1,5,1,2,1,sent==0); l.disposed(1,6,1,3,1,sent==0);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const {resourceAdmission: admission} = require('@pulse-compute/wasm-contracts/provider/toolchain');
|
|
5
|
+
const {resourceLedgerSource} = require('@pulse-compute/wasm-runtime-core-as/compiler/canonical-native');
|
|
6
|
+
const fixed = (id, bytes) => ({id, kind: 'fixed', bytes, copies: 1});
|
|
7
|
+
function profile(plan, bindings, headerBufferBytes) {
|
|
8
|
+
// Prove one visit and one decode from the canonical control tree, not merely
|
|
9
|
+
// the number of static effect sites. No authored accounting configuration.
|
|
10
|
+
const body = plan.entry?.body, effect = plan.effects[0];
|
|
11
|
+
const decoder = effect?.decoder;
|
|
12
|
+
if (plan.entry?.kind !== 'handler' || plan.handlers.length || plan.routing || plan.packages?.application
|
|
13
|
+
|| plan.effects.length !== 1 || effect.kind !== 'fetch' || effect.grouped || !['text','json'].includes(decoder)
|
|
14
|
+
|| body?.length !== 2 || body[0].kind !== 'effect' || body[0].effectId !== effect.id
|
|
15
|
+
|| body[1].kind !== 'return' || body[1].value?.name !== 'response.' + decoder
|
|
16
|
+
|| body[1].value.arguments.length !== 1 || body[1].value.arguments[0].kind !== 'local'
|
|
17
|
+
|| body[1].value.arguments[0].id !== effect.result?.localId) return null;
|
|
18
|
+
const init = effect.inputs.find(x => x.name === 'init')?.value;
|
|
19
|
+
const url = effect.inputs.find(x => x.name === 'url')?.value;
|
|
20
|
+
const uniqueKeys = object => object?.kind === 'object'
|
|
21
|
+
&& object.entries.every(x => x.kind === 'property' && x.key?.kind === 'literal' && typeof x.key.value === 'string')
|
|
22
|
+
&& new Set(object.entries.map(x => x.key.value)).size === object.entries.length;
|
|
23
|
+
const field = name => init?.entries?.find(x => x.key?.value === name)?.value;
|
|
24
|
+
const timeout = field('timeoutMs'), headers = field('headers');
|
|
25
|
+
const backend = bindings.backends.find(x => x.index === 0)?.backend;
|
|
26
|
+
const bound = plan.schemas?.registry?.maxBytes;
|
|
27
|
+
if (url?.kind !== 'literal' || typeof url.value !== 'string' || !uniqueKeys(init)
|
|
28
|
+
|| init.entries.some(x => !['timeoutMs','headers'].includes(x.key?.value))
|
|
29
|
+
|| timeout?.kind !== 'literal' || !Number.isSafeInteger(timeout.value) || timeout.value <= 0
|
|
30
|
+
|| (headers && (!uniqueKeys(headers) || headers.entries.some(x => x.key?.kind !== 'literal'
|
|
31
|
+
|| typeof x.key.value !== 'string' || x.value?.kind !== 'literal' || typeof x.value.value !== 'string')))
|
|
32
|
+
|| typeof backend !== 'string' || !backend || bindings.dynamicBackends
|
|
33
|
+
|| !Number.isInteger(bindings.maxDurationMs) || bindings.maxDurationMs < 1 || bindings.maxDurationMs > 30000
|
|
34
|
+
|| !Number.isInteger(bound) || bound <= 0 || bound > 1073741823) return null;
|
|
35
|
+
const realization = `fastly.native.fetch-${decoder}.single-v1`;
|
|
36
|
+
const requireJson = decoder === 'json' && plan.schemas.registry.contentTypePolicy === 'require-json';
|
|
37
|
+
// The header reader owns C bytes, key encoding and ABI outputs. AS 0.28.18
|
|
38
|
+
// decodeUnsafe allocates 2N bytes; toLowerCase allocates 4N before shrinking.
|
|
39
|
+
// The first value has N <= C-1 bytes because the list is NUL-terminated.
|
|
40
|
+
if (requireJson && (!Number.isInteger(headerBufferBytes) || headerBufferBytes < 1
|
|
41
|
+
|| headerBufferBytes > 2147483647)) throw new TypeError('Unproved JSON header backing');
|
|
42
|
+
const headerBytes = requireJson ? 7 * headerBufferBytes + 18 : 0;
|
|
43
|
+
const encodingBytes = 3 + Buffer.byteLength(url.value) + Buffer.byteLength(backend)
|
|
44
|
+
+ (headers?.entries || []).reduce((n,h) => n + Buffer.byteLength(h.key.value) + Buffer.byteLength(h.value.value), 0);
|
|
45
|
+
// 12 request outputs + 40 scheduler outputs + 8 clock + 8 reused body-wait
|
|
46
|
+
// outputs + 32 cleanup/exit outputs. Reader payload backing is 3B+5.
|
|
47
|
+
const scratchBytes = 100, readerBytes = 3 * bound + 5;
|
|
48
|
+
const work = {handles:[{namespace:'http.request',count:1},{namespace:'http.body',count:2},
|
|
49
|
+
{namespace:'http.pending',count:1},{namespace:'http.response',count:1}],
|
|
50
|
+
allocations:[fixed('scratch',scratchBytes),fixed('encoding',encodingBytes),fixed('reader',readerBytes),
|
|
51
|
+
...(requireJson ? [fixed('json-content-type',headerBytes)] : [])],records:1,cleanupSteps:0};
|
|
52
|
+
const cleanup = {handles:[],allocations:[],records:0,cleanupSteps:5};
|
|
53
|
+
const exit = {handles:[{namespace:'http.response',count:1},{namespace:'http.body',count:1}],
|
|
54
|
+
allocations:[fixed('error-text',64)],records:0,cleanupSteps:2};
|
|
55
|
+
const required = {handles:7,backingBytes:scratchBytes+encodingBytes+readerBytes+headerBytes+64,records:12+(requireJson?1:0),cleanupSteps:7,effects:1};
|
|
56
|
+
const result = admission.createPlan({version:admission.VERSION,realization,
|
|
57
|
+
selected:[{site:effect.id,count:1,profile:{version:admission.PROFILE_VERSION,owner:'provider-fastly',
|
|
58
|
+
operation:'single-fetch-'+decoder,realization,effects:1,work,cleanup,
|
|
59
|
+
exclusions:['incoming-request-context','application-values','diagnostic-storage','driver-identity-storage','successful-downstream-response','allocator-metadata',
|
|
60
|
+
...(decoder === 'json' ? ['json-parser-and-schema-materialization'] : [])]}}],
|
|
61
|
+
exit,occupied:Object.fromEntries(admission.DIMENSIONS.map(k=>[k,0])),limits:required});
|
|
62
|
+
if (!result.admitted || Object.keys(required).some(k => result.required[k] !== required[k])) throw new Error('Fetch resource inventory drift');
|
|
63
|
+
return Object.freeze({...result,encodingBytes,readerBytes,scratchBytes,headerBytes,storageBytes:1024+128*required.records,
|
|
64
|
+
...(requireJson ? {headerAdmission:{bufferBytes:headerBufferBytes,backingBytes:headerBytes}} : {}),
|
|
65
|
+
durationNs:bindings.maxDurationMs*1000000,selection:'canonical-single-get-'+decoder,
|
|
66
|
+
executionIdentity:'private-single-invocation-wasm-instance',
|
|
67
|
+
unprofiled:'other-fetch-shapes-repetition-groups-dynamic-inputs-and-javascript'});
|
|
68
|
+
}
|
|
69
|
+
function hooks(selected) {
|
|
70
|
+
if (!selected) return [];
|
|
71
|
+
const owner = 'fetch-accounting', order = 20;
|
|
72
|
+
return [
|
|
73
|
+
{point:'response.error',owner,order,body:' return __fca_error()'},
|
|
74
|
+
{point:'invocation.end',owner,order,body:' __fca_cleanup(true)'}
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
function source(selected) {
|
|
78
|
+
if (!selected) return '';
|
|
79
|
+
return resourceLedgerSource(true) + `\nconst __FCA_RECORDS:i32=${selected.required.records};\nconst __FCA_STORAGE:u32=${selected.storageBytes};\nconst __FCA_HEADER:u64=${selected.headerBytes};\nconst __FCA_BACKING:u64=${selected.required.backingBytes};\nconst __FCA_ENCODING:u64=${selected.encodingBytes};\nconst __FCA_READER:u64=${selected.readerBytes};\nconst __FCA_DURATION:u64=${selected.durationNs};\n`
|
|
80
|
+
+ fs.readFileSync(path.join(__dirname,'native-fetch-accounting.as.ts'),'utf8')
|
|
81
|
+
.replace('/*FCA_HEADER_STAGE*/', selected.headerBytes ? '|| !l.stageRecord(1,2,0,1,1,__FCA_HEADER)' : '')
|
|
82
|
+
.replace('/*FCA_HEADER_CONSUME*/', selected.headerBytes ? '&& l.consume(1,12,1,1,__FCA_HEADER,false,__fca_now())' : '');
|
|
83
|
+
}
|
|
84
|
+
module.exports = {profile,hooks,source};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Provider-local proof over canonical values. Unknown is not a zero allowance.
|
|
3
|
+
// Input materialization remains with the existing value/schema owners; this
|
|
4
|
+
// proves the strings and serializer copies consumed by the fetch service.
|
|
5
|
+
function create(plan,C,B){
|
|
6
|
+
const locals=new Map(),effects=new Map();let mutable=false;
|
|
7
|
+
const walk=(v,fn)=>{if(!v||typeof v!=='object')return;if(Array.isArray(v)){v.forEach(x=>walk(x,fn));return;}fn(v);Object.values(v).forEach(x=>walk(x,fn));};
|
|
8
|
+
walk(plan.entry,scan);walk(plan.handlers,scan);walk(plan.helpers,scan);walk(plan.stages,scan);
|
|
9
|
+
function scan(v){if(v.kind==='local'&&v.declaration==='const'&&v.value)locals.set(v.localId,v.value);if(/assign|update/.test(v.kind||'')||v.kind==='method-call'&&['push','pop','shift','unshift','splice'].includes(v.method))mutable=true;}
|
|
10
|
+
for(const e of plan.effects)if(e.kind==='fetch'&&e.result?.localId&&e.result.valueKind!=='fetch-response'&&e.decoder==='text')effects.set(e.result.localId,{chars:B,bytes:3*B});
|
|
11
|
+
function string(v,seen=new Set()){
|
|
12
|
+
if(!v)return null;
|
|
13
|
+
if(v.kind==='literal'&&typeof v.value==='string')return{chars:v.value.length,bytes:Buffer.byteLength(v.value)};
|
|
14
|
+
if(v.kind==='local'){
|
|
15
|
+
if(mutable)return null;
|
|
16
|
+
if(effects.has(v.id))return effects.get(v.id);
|
|
17
|
+
if(seen.has(v.id))return null;seen=new Set(seen);seen.add(v.id);return string(locals.get(v.id),seen);
|
|
18
|
+
}
|
|
19
|
+
if(v.kind==='context-read'&&v.path?.[0]==='req'&&['method','url','path'].includes(v.path[1]))return{chars:C,bytes:3*C};
|
|
20
|
+
if(v.kind==='intrinsic'&&v.name==='request.header')return{chars:C,bytes:3*C};
|
|
21
|
+
if(v.kind==='intrinsic'&&v.name==='request.text')return{chars:B,bytes:3*B};
|
|
22
|
+
if(v.kind==='method-call'&&v.receiver?.valueKind==='fetch-response'&&v.method==='text')return{chars:B,bytes:3*B};
|
|
23
|
+
if(v.kind==='binary'&&['+','||','??'].includes(v.operator)){
|
|
24
|
+
const a=string(v.left,seen),b=string(v.right,seen);if(!a||!b)return null;
|
|
25
|
+
return v.operator==='+'?{chars:a.chars+b.chars,bytes:a.bytes+b.bytes}:{chars:Math.max(a.chars,b.chars),bytes:Math.max(a.bytes,b.bytes)};
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function json(v,seen=new Set()){
|
|
30
|
+
if(!v)return null;
|
|
31
|
+
if(v.kind==='local'){
|
|
32
|
+
if(mutable||seen.has(v.id))return null;seen=new Set(seen);seen.add(v.id);return json(locals.get(v.id),seen);
|
|
33
|
+
}
|
|
34
|
+
const s=string(v,seen);if(s)return{chars:6*s.chars+2,nodes:1,numbers:0};
|
|
35
|
+
if(v.kind==='literal'&&(v.value===null||typeof v.value==='boolean'))return{chars:5,nodes:1,numbers:0};
|
|
36
|
+
if(v.kind==='literal'&&typeof v.value==='number')return{chars:32,nodes:1,numbers:1};
|
|
37
|
+
let children;
|
|
38
|
+
if(v.kind==='array')children=v.items;
|
|
39
|
+
else if(v.kind==='object'&&v.entries.every(e=>e.kind==='property'&&e.key?.kind==='literal'&&typeof e.key.value==='string'))children=v.entries.flatMap(e=>[e.key,e.value]);
|
|
40
|
+
else return null;
|
|
41
|
+
if(!Array.isArray(children))return null;
|
|
42
|
+
const parts=children.map(x=>json(x,seen));if(parts.some(x=>!x))return null;
|
|
43
|
+
return parts.reduce((a,b)=>({chars:a.chars+b.chars+1,nodes:a.nodes+b.nodes,numbers:a.numbers+b.numbers}),{chars:2,nodes:1,numbers:0});
|
|
44
|
+
}
|
|
45
|
+
return{string,json};
|
|
46
|
+
}
|
|
47
|
+
module.exports={create};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// One immutable invocation partition per existing driver ticket. No recycling,
|
|
2
|
+
// second effect counter, or scheduling authority is introduced here.
|
|
3
|
+
let __fetch_ra:PulseResourceLedger|null=null;
|
|
4
|
+
let __fca_origin:i64=0;
|
|
5
|
+
let __fca_clock_failed:bool=false;
|
|
6
|
+
let __fca_frame:i32=0;
|
|
7
|
+
const __fca_reaped=new StaticArray<bool>(__FCA_COUNT);
|
|
8
|
+
const __fca_io:usize=memory.data(32);
|
|
9
|
+
const __fca_exit:usize=memory.data(64);
|
|
10
|
+
function __fca_slot(local:i32):i32{return local>=5?__FCA_HANDLES-2+local-5:__fca_frame*5+local;}
|
|
11
|
+
function __fca_ns(slot:i32):u32{return slot==__FCA_HANDLES-2?2:slot==__FCA_HANDLES-1?3:slot%5==0?1:slot%5==2?4:slot%5==3?2:3;}
|
|
12
|
+
function __fca_now():u64{
|
|
13
|
+
if(__fca_clock_failed)return PRL_SAFE+1;
|
|
14
|
+
const now=__request_clock();
|
|
15
|
+
if(now<__fca_origin||__request_expired){__fca_clock_failed=true;return PRL_SAFE+1;}
|
|
16
|
+
if(now>=__request_deadline)__request_expired=true;
|
|
17
|
+
return u64(now-__fca_origin);
|
|
18
|
+
}
|
|
19
|
+
function __fca_ok(ok:bool):bool{if(!ok)__pulse_fastly_fail(PULSE_ERROR_HOSTCALL,272,-1);return ok;}
|
|
20
|
+
function __fca_work():bool{return __fca_ok(__fca_now()<__FCA_DURATION);}
|
|
21
|
+
function __fca_start(index:i32):bool{
|
|
22
|
+
const ticket=__pulse_invocation_tickets[index];
|
|
23
|
+
if(ticket<=0||ticket>__FCA_COUNT||ticket!=__pulse_invocation_count)return __fca_ok(false);
|
|
24
|
+
__fca_frame=ticket-1;
|
|
25
|
+
if(__fetch_ra!==null)return __fca_work();
|
|
26
|
+
if(__request_deadline<0||__request_last_clock<0)return __fca_ok(false);
|
|
27
|
+
__fca_origin=__request_deadline-i64(__FCA_DURATION);
|
|
28
|
+
const l=new PulseResourceLedger(1,__FCA_RECORDS,__FCA_HANDLES,__FCA_BACKING,5*__FCA_COUNT+2,__FCA_COUNT,__FCA_DURATION,__FCA_STORAGE);__fetch_ra=l;
|
|
29
|
+
if(!l.begin(1,__fca_now(),5*__FCA_COUNT,2,__FCA_COUNT,true))return __fca_ok(false);
|
|
30
|
+
for(let i=0;i<__FCA_HANDLES;i++)if(!l.stageRecord(1,1,__fca_ns(i),1,i>=__FCA_HANDLES-2?3:1,0))return __fca_ok(false);
|
|
31
|
+
if(!l.stageRecord(1,2,0,1,1,__FCA_SCRATCH)||!l.stageRecord(1,2,0,1,1,__FCA_ENCODING)||!l.stageRecord(1,2,0,1,1,__FCA_READER)||!l.stageRecord(1,2,0,1,3,64)||!l.stageRecord(1,3,0,1,1,0)||!l.commit(1,__fca_now()))return __fca_ok(false);
|
|
32
|
+
for(let i=0;i<__FCA_RECORDS;i++)if(l.reservedSlot(i)!=i||l.reservedGeneration(i)!=1)return __fca_ok(false);
|
|
33
|
+
return __fca_ok(l.consume(1,__FCA_HANDLES,1,1,__FCA_SCRATCH,false,__fca_now())&&l.consume(1,__FCA_HANDLES+1,1,1,__FCA_ENCODING,false,__fca_now())&&l.consume(1,__FCA_HANDLES+2,1,1,__FCA_READER,false,__fca_now()));
|
|
34
|
+
}
|
|
35
|
+
function __fca_acquire(local:i32):bool{const s=__fca_slot(local);return __fetch_ra!==null&&__fca_ok(__fetch_ra!.beginAcquire(1,s,1,__fca_ns(s),1,local>=5,__fca_now()));}
|
|
36
|
+
function __fca_acquired(local:i32,status:i32,handle:i32):i32{const l=__fetch_ra!,s=__fca_slot(local);if(status==0)__fca_ok(l.acquired(1,s,1,__fca_ns(s),1,u32(handle)));else l.noAcquisition(1,s,1,__fca_ns(s),1,false);return status;}
|
|
37
|
+
function __fca_new(local:i32,out:usize):i32{if(!__fca_acquire(local))return 1;const ns=__fca_ns(__fca_slot(local));return __fca_acquired(local,ns==1?fastly_http_req_new(out):ns==2?fastly_http_resp_new(out):fastly_http_body_new(out),load<i32>(out));}
|
|
38
|
+
function __fca_dispose_slot(s:i32):i32{
|
|
39
|
+
if(__fetch_ra===null)return 1;const l=__fetch_ra!,ns=__fca_ns(s);
|
|
40
|
+
if(l.bank[s*8+5]!=PRL_LIVE||!l.beginDispose(1,s,1,ns,1,__fca_now()))return 1;
|
|
41
|
+
const h=i32(l.bank[s*8+6]);const status=ns==1?__pulse_fetch_close_request(h):ns==2?__pulse_fetch_close_response(h):__pulse_fetch_close_body(h);l.disposed(1,s,1,ns,1,status==0);return status;
|
|
42
|
+
}
|
|
43
|
+
function __fca_dispose(local:i32):i32{return __fca_dispose_slot(__fca_slot(local));}
|
|
44
|
+
function __fca_dispatch(request:i32,body:i32,backend:usize,length:i32,out:usize):i32{
|
|
45
|
+
if(!__fca_acquire(2)||!__fca_acquire(3)||!__fca_acquire(4))return 1;
|
|
46
|
+
const l=__fetch_ra!,b=__fca_frame*5;
|
|
47
|
+
if(!l.beginDispose(1,b,1,1,1,__fca_now())||!l.beginDispose(1,b+1,1,3,1,__fca_now()))return 1;
|
|
48
|
+
const status=fastly_http_req_send_async(request,body,backend,length,out);
|
|
49
|
+
l.disposed(1,b,1,1,1,status==0);l.disposed(1,b+1,1,3,1,status==0);return __fca_acquired(2,status,load<i32>(out));
|
|
50
|
+
}
|
|
51
|
+
function __fca_find(handle:i32,local:i32):bool{
|
|
52
|
+
if(__fetch_ra===null)return false;const l=__fetch_ra!;
|
|
53
|
+
for(let i=0;i<__FCA_COUNT;i++){const s=(i*5+local)*8;if(l.bank[s+5]==PRL_LIVE&&l.bank[s+6]==u32(handle)){__fca_frame=i;return true;}}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function __fca_poll(handle:i32,detail:usize,done:usize,response:usize,body:usize):i32{
|
|
57
|
+
if(!__fca_find(handle,2)||__fca_now()>=__FCA_DURATION)return 1;
|
|
58
|
+
const l=__fetch_ra!,b=__fca_frame*5;
|
|
59
|
+
const status=__pulse_fetch_poll(handle,detail,done,response,body),ready=load<i32>(done);
|
|
60
|
+
if(status!=0||(ready!=0&&ready!=1)){l.uncertain(1,b+2,1,4,1);l.noAcquisition(1,b+3,1,2,1,false);l.noAcquisition(1,b+4,1,3,1,false);return status==0?2:status;}
|
|
61
|
+
if(ready==1){l.transferred(1,b+2,1,4,1);__fca_acquired(3,0,load<i32>(response));__fca_acquired(4,0,load<i32>(body));}return 0;
|
|
62
|
+
}
|
|
63
|
+
function __fca_unreaped():i32{if(__fetch_ra===null)return 0;let n=0;for(let i=0;i<__FCA_COUNT;i++){const s=__fetch_ra!.bank[(i*5+2)*8+5];if(s!=PRL_FREE&&s!=PRL_RESERVED)n++;}return n;}
|
|
64
|
+
function __fca_cleanup(final:bool=false):void{
|
|
65
|
+
if(__fetch_ra===null)return;const l=__fetch_ra!;l.close(1);
|
|
66
|
+
for(let i=0;i<__FCA_COUNT;i++)if(!__fca_reaped[i]&&l.bank[(i*5+2)*8+5]==PRL_LIVE){__fca_reaped[i]=true;if(l.cleanupStep(1,false,__fca_now())){__pulse_send_error_detail_reset(__fca_io);store<i32>(__fca_io+20,0);__fca_poll(i32(l.bank[(i*5+2)*8+6]),__fca_io,__fca_io+20,__fca_io+24,__fca_io+28);}}
|
|
67
|
+
for(let s=0;s<__FCA_HANDLES;s++){const state=l.bank[s*8+5];if(state==PRL_RESERVED&&(s<__FCA_HANDLES-2||final))l.unused(1,s,1,__fca_ns(s),1);else if(state==PRL_LIVE&&__fca_ns(s)!=4)__fca_dispose_slot(s);}
|
|
68
|
+
}
|
|
69
|
+
function __fca_transfer(response:i32,body:i32):i32{
|
|
70
|
+
if(!__fca_find(response,3)||!__fca_work())return 1;
|
|
71
|
+
const l=__fetch_ra!,b=__fca_frame*5;
|
|
72
|
+
if(l.bank[(b+4)*8+6]!=u32(body)||!l.beginDispose(1,b+3,1,2,1,__fca_now())||!l.beginDispose(1,b+4,1,3,1,__fca_now()))return 1;
|
|
73
|
+
const status=fastly_http_resp_send_downstream(response,body,0);l.disposed(1,b+3,1,2,1,status==0);l.disposed(1,b+4,1,3,1,status==0);return status;
|
|
74
|
+
}
|
|
75
|
+
function __fca_write(body:i32,value:string):i32{
|
|
76
|
+
const bytes=__pulse_fastly_utf8(value);let offset=0;
|
|
77
|
+
while(offset<bytes.byteLength){if(!__fca_work())return 1;store<i32>(__fca_io+20,0);const status=fastly_http_body_write(body,changetype<usize>(bytes)+offset,bytes.byteLength-offset,0,__fca_io+20),n=load<i32>(__fca_io+20);if(status!=0)return status;if(n<=0||n>bytes.byteLength-offset)return 1;offset+=n;}return 0;
|
|
78
|
+
}
|
|
79
|
+
class __FCA_Header{ name:string;value:string|null;constructor(name:string,value:string|null){this.name=name;this.value=value;} }
|
|
80
|
+
function __fca_header(response:__PulseFastlyValue,name:string):string|null{
|
|
81
|
+
if(response.fetchHeaders===null)response.fetchHeaders=new Array<__FCA_Header>();
|
|
82
|
+
for(let i=0;i<response.fetchHeaders!.length;i++){const h=response.fetchHeaders![i];if(h.name==name)return h.value;}
|
|
83
|
+
if(__fca_header_id(name)<0||!__fca_find(response.responseHandle,3)||!__fca_work()){__fca_ok(false);return null;}
|
|
84
|
+
const value=__pulse_header_lookup(response.responseHandle,name,true);response.fetchHeaders!.push(new __FCA_Header(name,value));return value;
|
|
85
|
+
}
|
|
86
|
+
function __fca_json_type(response:__PulseFastlyValue):bool{
|
|
87
|
+
if(response.fetchJsonType==0){const value=__fca_header(response,'content-type');response.fetchJsonType=value!==null&&value!.length>0&&value!.toLowerCase().indexOf('json')>=0?1:-1;}
|
|
88
|
+
return response.fetchJsonType==1;
|
|
89
|
+
}
|
|
90
|
+
function __fca_status(response:__PulseFastlyValue):i32{
|
|
91
|
+
if(!__fca_find(response.responseHandle,3)||!__fca_work())return 0;
|
|
92
|
+
if(fastly_http_resp_status_get(response.responseHandle,__fca_io+20)!=0){__pulse_fastly_fail(PULSE_ERROR_HOSTCALL,34,-1);return 0;}
|
|
93
|
+
return host_value_number(load<i32>(__fca_io+20));
|
|
94
|
+
}
|
|
95
|
+
function __fca_error():bool {
|
|
96
|
+
if(__fetch_ra===null) return false;
|
|
97
|
+
const l=__fetch_ra!;
|
|
98
|
+
// Capture the primary driver failure before cleanup or protected delivery.
|
|
99
|
+
const status=__request_expired || __pulse_fastly_error_stage==271 ? 504
|
|
100
|
+
: __pulse_fastly_last_error==PULSE_ERROR_TRANSPORT ? 502 : 500;
|
|
101
|
+
if(l.primary==0) l.fail(1,u32(status));
|
|
102
|
+
__fca_cleanup();
|
|
103
|
+
if(!__pulse_response_owned() || !l.consume(1,__FCA_HANDLES+3,1,1,64,true,__fca_now())) return true;
|
|
104
|
+
if(__fca_new(5,__fca_io+24)!=0) return true;
|
|
105
|
+
const response=load<i32>(__fca_io+24);
|
|
106
|
+
if(__fca_new(6,__fca_io+28)!=0) return true;
|
|
107
|
+
const body=load<i32>(__fca_io+28);
|
|
108
|
+
if(__fca_now()>=__FCA_DURATION || fastly_http_resp_status_set(response,status)!=0) return true;
|
|
109
|
+
const header='content-typetext/plain; charset=utf-8';
|
|
110
|
+
for(let i=0;i<header.length;i++) store<u8>(__fca_exit+i,u8(header.charCodeAt(i)));
|
|
111
|
+
if(__fca_now()>=__FCA_DURATION || fastly_http_resp_header_append(response,__fca_exit,12,__fca_exit+12,header.length-12)!=0) return true;
|
|
112
|
+
const text=status==504?'Gateway Timeout':status==502?'Bad Gateway':'Internal Server Error';
|
|
113
|
+
for(let i=0;i<text.length;i++) store<u8>(__fca_exit+i,u8(text.charCodeAt(i)));
|
|
114
|
+
let offset=0;
|
|
115
|
+
while(offset<text.length) {
|
|
116
|
+
if(__fca_now()>=__FCA_DURATION) return true;
|
|
117
|
+
const result=fastly_http_body_write(body,__fca_exit+offset,text.length-offset,0,__fca_io+20);
|
|
118
|
+
const written=load<i32>(__fca_io+20);
|
|
119
|
+
if(result!=0 || written<=0 || written>text.length-offset) return true;
|
|
120
|
+
offset+=written;
|
|
121
|
+
}
|
|
122
|
+
if(!l.beginDispose(1,__FCA_HANDLES-2,1,2,1,__fca_now()) || !l.beginDispose(1,__FCA_HANDLES-1,1,3,1,__fca_now())) return true;
|
|
123
|
+
const sent=fastly_http_resp_send_downstream(response,body,0);
|
|
124
|
+
l.disposed(1,__FCA_HANDLES-2,1,2,1,sent==0); l.disposed(1,__FCA_HANDLES-1,1,3,1,sent==0);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs=require('node:fs'),path=require('node:path');
|
|
3
|
+
const {resourceAdmission:admission}=require('@pulse-compute/wasm-contracts/provider/toolchain');
|
|
4
|
+
const {resourceLedgerSource}=require('@pulse-compute/wasm-runtime-core-as/compiler/canonical-native');
|
|
5
|
+
const {CANONICAL_EFFECT_INVOCATION_POLICY:policy}=require('@pulse-compute/wasm-contracts/handler/canonical-runtime');
|
|
6
|
+
function profile(plan,bindings,C){
|
|
7
|
+
if(!plan.effects.length || plan.effects.some(e=>e.kind!=='fetch') || plan.capabilities.includes('request.body.forward')
|
|
8
|
+
|| plan.packages?.application || plan.callables?.length || plan.routing?.entries?.some(e=>e.kind==='error') || bindings.dynamicBackends
|
|
9
|
+
|| !Number.isInteger(bindings.maxDurationMs)||bindings.maxDurationMs<1||bindings.maxDurationMs>30000)return null;
|
|
10
|
+
const B=plan.schemas?.registry?.maxBytes;
|
|
11
|
+
if(!Number.isInteger(B)||B<1||B>1073741823||!Number.isInteger(C)||C<9||C>1073741823)return null;
|
|
12
|
+
const fields=o=>o?.kind==='object'&&o.entries.every(e=>e.kind==='property'&&e.key?.kind==='literal'&&typeof e.key.value==='string')&&new Set(o.entries.map(e=>e.key.value)).size===o.entries.length?new Map(o.entries.map(e=>[e.key.value,e.value])):null;
|
|
13
|
+
const proof=require('./native-fetch-bounds.js').create(plan,C,B);
|
|
14
|
+
let encoding=0,caseBytes=0;
|
|
15
|
+
const literal=value=>({kind:'literal',value});
|
|
16
|
+
for(let i=0;i<plan.effects.length;i++){
|
|
17
|
+
const e=plan.effects[i],url=e.inputs.find(x=>x.name==='url')?.value,init=e.inputs.find(x=>x.name==='init')?.value;
|
|
18
|
+
const f=init&&init.kind!=='undefined'?fields(init):new Map(),backend=bindings.backends.find(b=>b.index===i)?.backend;
|
|
19
|
+
if(!f||[...f.keys()].some(k=>!['method','body','json','headers','timeoutMs'].includes(k))||!backend)return null;
|
|
20
|
+
const u=proof.string(url),method=proof.string(f.get('method')||literal('GET'));
|
|
21
|
+
if(!u||!method)return null;
|
|
22
|
+
const headers=f.has('headers')?fields(f.get('headers')):new Map();if(!headers)return null;
|
|
23
|
+
const values=[...headers.values()].map(v=>proof.string(v));if(values.some(v=>!v))return null;
|
|
24
|
+
const timeout=f.get('timeoutMs');if(timeout&&(timeout.kind!=='literal'||!Number.isSafeInteger(timeout.value)||timeout.value<=0))return null;
|
|
25
|
+
let body=proof.string(f.get('body')||literal('')),materialization=f.has('body')&&f.get('body').kind!=='literal'?128:0,extraHeader=0;
|
|
26
|
+
const json=f.has('body')&&!body?f.get('body'):!f.has('body')&&f.has('json')?f.get('json'):null;
|
|
27
|
+
if(json){const j=proof.json(json);if(!j)return null;body={chars:j.chars,bytes:3*j.chars};
|
|
28
|
+
// Quote buffers + quoted strings + final join, geometric parts-array
|
|
29
|
+
// growth, and a conservative pinned numeric formatter allowance.
|
|
30
|
+
materialization=8*j.chars+128*j.nodes+128+1024*j.numbers;
|
|
31
|
+
if(f.has('json')){extraHeader=Buffer.byteLength('content-typeapplication/json; charset=utf-8');materialization+=[...headers.keys()].reduce((n,k)=>n+4*k.length,0);}
|
|
32
|
+
}
|
|
33
|
+
if(!body)return null;
|
|
34
|
+
encoding=Math.max(encoding,u.bytes+Buffer.byteLength(backend)+(f.has('method')?9*method.chars:3)+body.bytes+extraHeader+[...headers.keys()].reduce((n,k)=>n+Buffer.byteLength(k),0)+values.reduce((n,v)=>n+v.bytes,0));
|
|
35
|
+
caseBytes=Math.max(caseBytes,(f.has('method')?6*method.chars:0)+materialization);
|
|
36
|
+
}
|
|
37
|
+
const headerNames=new Set(plan.schemas.registry.contentTypePolicy==='require-json'?['content-type']:[]);
|
|
38
|
+
let repeated=false,unsupported=false;
|
|
39
|
+
function visit(v){if(!v||typeof v!=='object')return;if(Array.isArray(v)){v.forEach(visit);return;}
|
|
40
|
+
if(/loop|while|for|map/.test(v.kind||'')||['helper-call','stage-call'].includes(v.kind))repeated=true;
|
|
41
|
+
if(v.kind==='method-call'&&v.receiver?.valueKind==='fetch-response'&&v.method==='header'){
|
|
42
|
+
const name=v.arguments?.[0];if(name?.kind!=='literal'||typeof name.value!=='string')unsupported=true;else headerNames.add(name.value);
|
|
43
|
+
}
|
|
44
|
+
Object.values(v).forEach(visit);
|
|
45
|
+
}
|
|
46
|
+
visit(plan.entry);visit(plan.handlers);visit(plan.helpers);visit(plan.stages);if(unsupported)return null;
|
|
47
|
+
const count=Math.min(policy.defaultMaxEffects,repeated?policy.defaultMaxEffects:plan.effects.length),header=[...headerNames].sort();
|
|
48
|
+
// Each response owns one cached decode per admitted name. Lowercase admission
|
|
49
|
+
// is also cached; repeated aliases never allocate another fetch-service copy.
|
|
50
|
+
const headerBytes=header.reduce((n,k)=>n+3*C+Buffer.byteLength(k)+11+(k==='content-type'?4*(C-1):0),0)+(header.length?48*header.length+32:0);
|
|
51
|
+
const scratch=12*count+(36+4*plan.effects.length)*count+52+count;
|
|
52
|
+
const backing=scratch+count*(encoding+caseBytes+3*B+5+headerBytes)+64;
|
|
53
|
+
const handles=5*count+2,records=handles+5,steps=5*count+2;
|
|
54
|
+
if(!Number.isSafeInteger(backing)||backing>9007199254740991)return null;
|
|
55
|
+
const fixed=(id,bytes)=>({id,kind:'fixed',bytes,copies:1});
|
|
56
|
+
const work={handles:[{namespace:'http.request',count},{namespace:'http.body',count:2*count},{namespace:'http.pending',count},{namespace:'http.response',count}],allocations:[fixed('service-scratch',scratch),fixed('encodings',count*(encoding+caseBytes)),fixed('reader-and-headers',count*(3*B+5+headerBytes))],records:1,cleanupSteps:0};
|
|
57
|
+
const exit={handles:[{namespace:'http.response',count:1},{namespace:'http.body',count:1}],allocations:[fixed('error-text',64)],records:0,cleanupSteps:2};
|
|
58
|
+
const limits={handles,backingBytes:backing,records,cleanupSteps:steps,effects:count};
|
|
59
|
+
const realization='fastly.native.fetch.cohorts-v1';
|
|
60
|
+
const result=admission.createPlan({version:admission.VERSION,realization,selected:[{site:'fetch-invocation',count:1,profile:{version:admission.PROFILE_VERSION,owner:'provider-fastly',operation:'bounded-fetch-invocation',realization,effects:count,work,cleanup:{handles:[],allocations:[],records:0,cleanupSteps:5*count},exclusions:['application-values','json-parser-and-schema-materialization','incoming-request-context','diagnostic-storage','driver-identity-storage','successful-downstream-response','allocator-metadata']}}],exit,occupied:Object.fromEntries(admission.DIMENSIONS.map(k=>[k,0])),limits});
|
|
61
|
+
if(!result.admitted||admission.DIMENSIONS.some(k=>result.required[k]!==limits[k]))throw Error('Fetch cohort profile drift');
|
|
62
|
+
return Object.freeze({...result,selection:'canonical-fetch-cohorts',executionIdentity:'private-single-invocation-wasm-instance',unprofiled:'unproved-inputs-dynamic-backends-mixed-capabilities-error-recovery-application-packages-child-graphs-and-other-targets',count,headerNames:header,headerBytes,scratchBytes:scratch,encodingBytes:count*(encoding+caseBytes),readerBytes:count*(3*B+5+headerBytes),storageBytes:1024+128*records,durationNs:bindings.maxDurationMs*1000000});
|
|
63
|
+
}
|
|
64
|
+
function source(p){return resourceLedgerSource(true)+`\nconst __FCA_COUNT:i32=${p.count};\nconst __FCA_HANDLES:i32=${p.required.handles};\nconst __FCA_RECORDS:i32=${p.required.records};\nconst __FCA_BACKING:u64=${p.required.backingBytes};\nconst __FCA_STORAGE:u32=${p.storageBytes};\nconst __FCA_DURATION:u64=${p.durationNs};\nconst __FCA_SCRATCH:u64=${p.scratchBytes};\nconst __FCA_ENCODING:u64=${p.encodingBytes};\nconst __FCA_READER:u64=${p.readerBytes};\nfunction __fca_header_id(name:string):i32 { ${p.headerNames.map((s,i)=>`if(name==${JSON.stringify(s)})return ${i};`).join('')} return -1; }\n`+fs.readFileSync(path.join(__dirname,'native-fetch-cohorts.as.ts'),'utf8');}
|
|
65
|
+
module.exports={profile,source};
|