@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,153 @@
|
|
|
1
|
+
// One ledger is bound to this Wasm instance's single managed invocation. Raw
|
|
2
|
+
// memory/ABI access is trusted; no scalar resource identity crosses the host ABI.
|
|
3
|
+
// The entry guard prevents reuse; this closed, one-cohort profile never recycles
|
|
4
|
+
// slots. Descriptor positions/generation 1 are validated at the commit boundary.
|
|
5
|
+
let __fwd_ra: PulseResourceLedger | null = null;
|
|
6
|
+
let __fra_origin: i64 = 0;
|
|
7
|
+
let __fra_clock_failed: bool = false;
|
|
8
|
+
let __fra_reaped: bool = false;
|
|
9
|
+
function __fra_ns(slot: i32): u32 {
|
|
10
|
+
return slot == 0 || slot == 2 ? 1 : slot == 4 ? 4 : slot == 5 || slot == 7 || slot == 9 ? 2 : 3;
|
|
11
|
+
}
|
|
12
|
+
function __fra_now(): u64 {
|
|
13
|
+
if (__fra_clock_failed) return PRL_SAFE + 1;
|
|
14
|
+
const now = __request_clock();
|
|
15
|
+
if (now < __fra_origin || __request_expired) { __fra_clock_failed = true; return PRL_SAFE + 1; }
|
|
16
|
+
return u64(now - __fra_origin);
|
|
17
|
+
}
|
|
18
|
+
function __fra_ok(ok: bool): bool {
|
|
19
|
+
if (!ok) __fwd_fail(__request_expired ? 504 : 502);
|
|
20
|
+
return ok;
|
|
21
|
+
}
|
|
22
|
+
function __fra_start(): bool {
|
|
23
|
+
if (__fwd_ra !== null || __request_deadline < 0 || __request_last_clock < 0) return false;
|
|
24
|
+
__fra_origin = __request_deadline - i64(__FRA_DURATION);
|
|
25
|
+
const ledger = new PulseResourceLedger(1, 15, 11, __FRA_BACKING, 12, 1, __FRA_DURATION, __FRA_STORAGE);
|
|
26
|
+
__fwd_ra = ledger;
|
|
27
|
+
if (!ledger.begin(1, __fra_now(), 10, 2, 1, true)) return __fra_ok(false);
|
|
28
|
+
for (let i = 0; i < 11; i++)
|
|
29
|
+
if (!ledger.stageRecord(1, 1, __fra_ns(i), 1, i >= 9 ? 3 : 1, 0)) return __fra_ok(false);
|
|
30
|
+
if (!ledger.stageRecord(1, 2, 0, 1, 1, 33920)
|
|
31
|
+
|| !ledger.stageRecord(1, 2, 0, 1, 1, __FRA_ENCODING)
|
|
32
|
+
|| !ledger.stageRecord(1, 2, 0, 1, 3, 64)
|
|
33
|
+
|| !ledger.stageRecord(1, 3, 0, 1, 1, 0)
|
|
34
|
+
|| !ledger.commit(1, __fra_now())) return __fra_ok(false);
|
|
35
|
+
for (let i = 0; i < 15; i++)
|
|
36
|
+
if (ledger.reservedSlot(i) != i || ledger.reservedGeneration(i) != 1) return __fra_ok(false);
|
|
37
|
+
// Static scratch is present in the artifact, charged once before first use.
|
|
38
|
+
// Encoding allowance is consumed before any request/header UTF-8 allocation.
|
|
39
|
+
return __fra_ok(ledger.consume(1,11,1,1,33920,false,__fra_now())
|
|
40
|
+
&& ledger.consume(1,12,1,1,__FRA_ENCODING,false,__fra_now()));
|
|
41
|
+
}
|
|
42
|
+
function __fra_acquire(slot: i32): bool {
|
|
43
|
+
return __fwd_ra !== null && __fra_ok(__fwd_ra!.beginAcquire(1,slot,1,__fra_ns(slot),1,slot >= 9,__fra_now()));
|
|
44
|
+
}
|
|
45
|
+
function __fra_acquired(slot: i32, status: i32, handle: i32): i32 {
|
|
46
|
+
const ledger = __fwd_ra!;
|
|
47
|
+
if (status == 0) __fra_ok(ledger.acquired(1,slot,1,__fra_ns(slot),1,u32(handle)));
|
|
48
|
+
else { ledger.noAcquisition(1,slot,1,__fra_ns(slot),1,false); __fwd_fail(502); }
|
|
49
|
+
return status;
|
|
50
|
+
}
|
|
51
|
+
function __fra_incoming(request: usize, body: usize): i32 {
|
|
52
|
+
if (!__fra_start() || !__fra_acquire(0) || !__fra_acquire(1)) return 1;
|
|
53
|
+
const status = fastly_http_req_body_downstream_get(request,body);
|
|
54
|
+
__fra_acquired(0,status,load<i32>(request)); __fra_acquired(1,status,load<i32>(body));
|
|
55
|
+
return status;
|
|
56
|
+
}
|
|
57
|
+
function __fra_new(slot: i32, out: usize): i32 {
|
|
58
|
+
if (!__fra_acquire(slot)) return 1;
|
|
59
|
+
const ns = __fra_ns(slot);
|
|
60
|
+
const status = ns == 1 ? fastly_http_req_new(out) : ns == 2 ? fastly_http_resp_new(out) : fastly_http_body_new(out);
|
|
61
|
+
return __fra_acquired(slot,status,load<i32>(out));
|
|
62
|
+
}
|
|
63
|
+
function __fra_dispose(slot: i32, abandon: bool = false): i32 {
|
|
64
|
+
if (__fwd_ra === null) return 1;
|
|
65
|
+
const ledger = __fwd_ra!, at = slot * 8;
|
|
66
|
+
if (ledger.bank[at+5] != PRL_LIVE) return 1;
|
|
67
|
+
// Disposal does not replace a primary error and never replenishes time.
|
|
68
|
+
if (!ledger.beginDispose(1,slot,1,__fra_ns(slot),1,__fra_now())) return 1;
|
|
69
|
+
const handle = i32(ledger.bank[at+6]), ns = __fra_ns(slot);
|
|
70
|
+
const status = ns == 1 ? __pulse_fetch_close_request(handle) : ns == 2 ? __pulse_fetch_close_response(handle)
|
|
71
|
+
: abandon ? __fwd_abandon(handle) : __pulse_fetch_close_body(handle);
|
|
72
|
+
ledger.disposed(1,slot,1,ns,1,status == 0);
|
|
73
|
+
return status;
|
|
74
|
+
}
|
|
75
|
+
function __fra_dispatch(request: i32, body: i32, backend: usize, length: i32, out: usize): i32 {
|
|
76
|
+
if (__pulse_invocation_count != 1 || __pulse_invocation_tickets[__fwd_index] != 1
|
|
77
|
+
|| !__fra_acquire(4) || !__fra_acquire(5) || !__fra_acquire(6)) { __fwd_fail(502); return 1; }
|
|
78
|
+
const ledger = __fwd_ra!;
|
|
79
|
+
// Fence the possibly consumed request before send; upload stays guest-owned.
|
|
80
|
+
if (!ledger.beginDispose(1,2,1,1,1,__fra_now())) return 1;
|
|
81
|
+
const status = __fwd_dispatch(request,body,backend,length,out);
|
|
82
|
+
ledger.disposed(1,2,1,1,1,status == 0);
|
|
83
|
+
return __fra_acquired(4,status,load<i32>(out));
|
|
84
|
+
}
|
|
85
|
+
function __fra_poll(): i32 {
|
|
86
|
+
const ledger = __fwd_ra!;
|
|
87
|
+
// Outputs are fenced before dispatch and retained across qualified not-ready
|
|
88
|
+
// results. A failed/invalid poll is never retried, including during cleanup.
|
|
89
|
+
const status = __pulse_fetch_poll(__fwd_pending,__fwd_detail,__fwd_done,__fwd_response_out,__fwd_body_out);
|
|
90
|
+
const done = load<i32>(__fwd_done);
|
|
91
|
+
if (status != 0 || (done != 0 && done != 1)) {
|
|
92
|
+
ledger.uncertain(1,4,1,4,1);
|
|
93
|
+
ledger.noAcquisition(1,5,1,2,1,false); ledger.noAcquisition(1,6,1,3,1,false);
|
|
94
|
+
return status == 0 ? 1 : status;
|
|
95
|
+
}
|
|
96
|
+
if (done == 1) {
|
|
97
|
+
ledger.transferred(1,4,1,4,1);
|
|
98
|
+
__fra_acquired(5,0,load<i32>(__fwd_response_out)); __fra_acquired(6,0,load<i32>(__fwd_body_out));
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
function __fra_send(response: i32, body: i32, streaming: i32): i32 {
|
|
103
|
+
const ledger = __fwd_ra!, r = streaming == 1 ? 7 : 9, b = streaming == 1 ? 8 : 10;
|
|
104
|
+
if (!ledger.beginDispose(1,r,1,2,1,__fra_now())) return 1;
|
|
105
|
+
if (streaming == 0 && !ledger.beginDispose(1,b,1,3,1,__fra_now())) return 1;
|
|
106
|
+
const status = fastly_http_resp_send_downstream(response,body,streaming);
|
|
107
|
+
ledger.disposed(1,r,1,2,1,status == 0);
|
|
108
|
+
if (streaming == 0) ledger.disposed(1,b,1,3,1,status == 0);
|
|
109
|
+
return status;
|
|
110
|
+
}
|
|
111
|
+
function __fra_cleanup(final: bool = false): void {
|
|
112
|
+
if (__fwd_ra === null) return;
|
|
113
|
+
const ledger = __fwd_ra!;
|
|
114
|
+
ledger.close(1);
|
|
115
|
+
// One reserved, nonblocking observation may adopt already-fenced outputs.
|
|
116
|
+
// Never retry unknown outcomes, acquire new claims, or extend the deadline.
|
|
117
|
+
if (!__fra_reaped && ledger.bank[4*8+5] == PRL_LIVE) {
|
|
118
|
+
__fra_reaped = true;
|
|
119
|
+
if (ledger.cleanupStep(1,false,__fra_now())) {
|
|
120
|
+
__pulse_send_error_detail_reset(__fwd_detail); store<i32>(__fwd_done,0);
|
|
121
|
+
__fra_poll();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// No pending-request cancel ABI. Remaining claims stay held for host teardown.
|
|
125
|
+
for (let slot = 0; slot < 11; slot++) {
|
|
126
|
+
const state = ledger.bank[slot*8+5];
|
|
127
|
+
if (state == PRL_RESERVED && (slot < 9 || final)) ledger.unused(1,slot,1,__fra_ns(slot),1);
|
|
128
|
+
else if (state == PRL_LIVE && slot != 4) __fra_dispose(slot,slot == 3 || slot == 8);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function __fra_error(status: i32): void {
|
|
132
|
+
if (__fwd_ra === null || !__pulse_response_owned()) return;
|
|
133
|
+
const ledger = __fwd_ra!;
|
|
134
|
+
if (!ledger.consume(1,13,1,1,64,true,__fra_now())) return;
|
|
135
|
+
// A fixed exit body uses the already-reserved scratch, without serialization
|
|
136
|
+
// or an application value allocation. Authority fences the one final send.
|
|
137
|
+
const text = status == 413 ? 'Payload Too Large' : status == 504 ? 'Gateway Timeout' : status == 400 ? 'Bad Request' : 'Bad Gateway';
|
|
138
|
+
if (__fra_new(9,__fwd_io+52) != 0) return;
|
|
139
|
+
const response = load<i32>(__fwd_io+52);
|
|
140
|
+
if (__fra_new(10,__fwd_io+52) != 0) return;
|
|
141
|
+
const body = load<i32>(__fwd_io+52);
|
|
142
|
+
if (__fra_now() >= __FRA_DURATION || fastly_http_resp_status_set(response,status) != 0) return;
|
|
143
|
+
for (let i = 0; i < text.length; i++) store<u8>(__fwd_buffer+i,u8(text.charCodeAt(i)));
|
|
144
|
+
let offset = 0;
|
|
145
|
+
while (offset < text.length) {
|
|
146
|
+
if (__fra_now() >= __FRA_DURATION) return;
|
|
147
|
+
const status = fastly_http_body_write(body,__fwd_buffer+offset,text.length-offset,0,__fwd_io+52);
|
|
148
|
+
const written = load<i32>(__fwd_io+52);
|
|
149
|
+
if (status != 0 || written <= 0 || written > text.length-offset) return;
|
|
150
|
+
offset += written;
|
|
151
|
+
}
|
|
152
|
+
if (__fra_send(response,body,0) == 0) __pulse_fastly_last_error = 0; // delivery handled; ledger primary remains
|
|
153
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Trusted provider profile producer. Selection follows the existing canonical
|
|
3
|
+
// forwarding validator; no authored profile, test limit or emitter callback.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { resourceAdmission: admission } = require('@pulse-compute/wasm-contracts/provider/toolchain');
|
|
7
|
+
const { resourceLedgerSource } = require('@pulse-compute/wasm-runtime-core-as/compiler/canonical-native');
|
|
8
|
+
const REALIZATION = 'fastly.native.forwarding.fixed-v1';
|
|
9
|
+
const empty = () => ({ handles: [], allocations: [], records: 0, cleanupSteps: 0 });
|
|
10
|
+
const fixed = (id, bytes) => ({ id, kind: 'fixed', bytes, copies: 1 });
|
|
11
|
+
function profile(plan, bindings) {
|
|
12
|
+
if (!plan.capabilities.includes('request.body.forward')) return null;
|
|
13
|
+
const effect = plan.effects[0];
|
|
14
|
+
const init = effect.inputs.find(x => x.name === 'init').value;
|
|
15
|
+
const headers = init.entries.find(x => x.key.value === 'headers')?.value.entries || [];
|
|
16
|
+
const url = effect.inputs.find(x => x.name === 'url').value;
|
|
17
|
+
const backend = bindings.backends.find(x => x.index === 0)?.backend;
|
|
18
|
+
if (plan.effects.length !== 1 || effect.grouped || url.kind !== 'literal' || typeof url.value !== 'string'
|
|
19
|
+
|| typeof backend !== 'string' || !backend || !Number.isInteger(bindings.maxDurationMs)
|
|
20
|
+
|| bindings.maxDurationMs < 1 || bindings.maxDurationMs > 30000) throw new TypeError('Unproved forwarding accounting profile');
|
|
21
|
+
const encodingBytes = Buffer.byteLength(url.value) + Buffer.byteLength(backend)
|
|
22
|
+
+ headers.reduce((n, h) => n + Buffer.byteLength(h.key.value) + Buffer.byteLength(h.value.value), 0);
|
|
23
|
+
const work = { handles: [{namespace:'http.request',count:2},{namespace:'http.body',count:4},
|
|
24
|
+
{namespace:'http.pending',count:1},{namespace:'http.response',count:2}],
|
|
25
|
+
allocations: [fixed('scratch',33920),fixed('encoding',encodingBytes)], records:1, cleanupSteps:0 };
|
|
26
|
+
const cleanup = {...empty(),cleanupSteps:10};
|
|
27
|
+
const exit = { handles:[{namespace:'http.response',count:1},{namespace:'http.body',count:1}],
|
|
28
|
+
allocations:[fixed('error-text',64)],records:0,cleanupSteps:2 };
|
|
29
|
+
const required = {handles:11,backingBytes:33984+encodingBytes,records:15,cleanupSteps:12,effects:1};
|
|
30
|
+
const result = admission.createPlan({version:admission.VERSION,realization:REALIZATION,
|
|
31
|
+
selected:[{site:'forward',count:1,profile:{version:admission.PROFILE_VERSION,owner:'provider-fastly',
|
|
32
|
+
operation:'bounded-forward',realization:REALIZATION,effects:1,work,cleanup,
|
|
33
|
+
exclusions:['request-context','value-wrappers','diagnostic-storage','driver-identity-storage','non-forwarding-driver-responses']}}],
|
|
34
|
+
exit,occupied:Object.fromEntries(admission.DIMENSIONS.map(k=>[k,0])),limits:required});
|
|
35
|
+
if (!result.admitted || Object.keys(required).some(k=>result.required[k]!==required[k])) throw new Error('Forwarding resource inventory drift');
|
|
36
|
+
const storageBytes = 1024 + result.required.records * 128;
|
|
37
|
+
if (storageBytes > 2147483647 || !Number.isSafeInteger(encodingBytes)) throw new RangeError('Forwarding resource representation');
|
|
38
|
+
return Object.freeze({ ...result, encodingBytes, storageBytes, durationNs:bindings.maxDurationMs*1000000,
|
|
39
|
+
selection:'canonical-terminal-forwarding', executionIdentity:'private-single-invocation-wasm-instance',
|
|
40
|
+
unprofiled:'general-fetch-decoding-cache-composition-and-javascript' });
|
|
41
|
+
}
|
|
42
|
+
function source(selected) {
|
|
43
|
+
if (!selected) return '';
|
|
44
|
+
return resourceLedgerSource(true) + `\nconst __FRA_ENCODING: u64 = ${selected.encodingBytes};\nconst __FRA_BACKING: u64 = ${selected.required.backingBytes};\nconst __FRA_STORAGE: u32 = ${selected.storageBytes};\nconst __FRA_DURATION: u64 = ${selected.durationNs};\n`
|
|
45
|
+
+ fs.readFileSync(path.join(__dirname,'native-forwarding-accounting.as.ts'),'utf8');
|
|
46
|
+
}
|
|
47
|
+
module.exports = { profile, source };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function validateRoots(plan, manifest) {
|
|
4
|
+
if (!manifest.handleOwnership?.enabled) return;
|
|
5
|
+
const { CANONICAL_NATIVE_PURE_HELPER_VERSION } = require('@pulse-compute/wasm-contracts/handler/canonical-native-plan');
|
|
6
|
+
const expected = ['__pulse_result',
|
|
7
|
+
...((plan.helpers || []).some(h => h.version !== CANONICAL_NATIVE_PURE_HELPER_VERSION) ? ['__pulse_helper_result'] : []),
|
|
8
|
+
...(plan.locals || []).map((_, i) => `__pulse_local_${i}`),
|
|
9
|
+
...(plan.effects || []).map((_, i) => `__pulse_effect_result_${i}`),
|
|
10
|
+
...(manifest.requestState.enabled ? ['__pulse_state_keys', '__pulse_state_values'] : [])];
|
|
11
|
+
if (JSON.stringify(manifest.handleOwnership.roots) !== JSON.stringify(expected)) {
|
|
12
|
+
throw new Error('Fastly handle ownership root descriptor mismatch');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function providerRoots(plan) {
|
|
17
|
+
return ['__pulse_request_headers_snapshot', '__pulse_fastly_payloads', '__pulse_fastly_ready',
|
|
18
|
+
'__pulse_handle_cache_roots', '__pulse_handle_resource_roots',
|
|
19
|
+
...((plan.capabilities || []).includes('request.body.forward') ? ['__fwd_marker', '__fwd_result'] : [])];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function providerSource(plan, enabled, roots = providerRoots(plan)) {
|
|
23
|
+
if (!enabled) return '';
|
|
24
|
+
if (JSON.stringify(roots) !== JSON.stringify(providerRoots(plan))) throw new Error('Fastly provider root descriptor mismatch');
|
|
25
|
+
const arrays = new Set(['__pulse_fastly_payloads', '__pulse_fastly_ready', '__pulse_handle_cache_roots', '__pulse_handle_resource_roots']);
|
|
26
|
+
return ['function __pulse_handle_provider_roots(): void {',
|
|
27
|
+
...roots.map(name => arrays.has(name)
|
|
28
|
+
? ` for (let i = 0; i < ${name}.length; i++) __pulse_handle_mark(unchecked(${name}[i]))`
|
|
29
|
+
: ` __pulse_handle_mark(${name})`), '}'].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// This source is composed only with the canonical emitter's complete, eligible
|
|
33
|
+
// root descriptor. Synchronous host operations never call the checkpoint.
|
|
34
|
+
function fields(enabled) {
|
|
35
|
+
return enabled ? `
|
|
36
|
+
traceHandleEdges(): void {
|
|
37
|
+
if (this.valuesStorage !== null) {
|
|
38
|
+
const values = this.valuesStorage!
|
|
39
|
+
for (let i = 0; i < values.length; i++) {
|
|
40
|
+
const handle = unchecked(values[i])
|
|
41
|
+
if (handle <= 0) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 1, -1); return }
|
|
42
|
+
__pulse_handle_mark(handle)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (this.textLeft !== null) __pulse_handle_work!.push(this.textLeft!)
|
|
46
|
+
if (this.textRight !== null) __pulse_handle_work!.push(this.textRight!)
|
|
47
|
+
}
|
|
48
|
+
` : '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tableSource(enabled) {
|
|
52
|
+
if (!enabled) return `
|
|
53
|
+
@lazy const __pulse_fastly_values = new Array<__PulseFastlyValue>()
|
|
54
|
+
function __pulse_fastly_has_value(handle: i32): bool { return handle > 0 && handle <= __pulse_fastly_values.length }
|
|
55
|
+
function __pulse_fastly_value_count(): i32 { return __pulse_fastly_values.length }
|
|
56
|
+
function __pulse_fastly_put(value: __PulseFastlyValue): i32 { __pulse_fastly_values.push(value); return __pulse_fastly_values.length }
|
|
57
|
+
function __pulse_fastly_value(handle: i32): __PulseFastlyValue { if (!__pulse_fastly_has_value(handle)) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 1, -1); return new __PulseFastlyValue() } return unchecked(__pulse_fastly_values[handle - 1]) }
|
|
58
|
+
`;
|
|
59
|
+
return `
|
|
60
|
+
@lazy let __pulse_fastly_values = new Map<i32, __PulseFastlyValue>()
|
|
61
|
+
// The existing caches never evict or replace entries. Record each insertion
|
|
62
|
+
// once so tracing is proportional to occupied slots, not 132,101 possible slots.
|
|
63
|
+
@lazy const __pulse_handle_cache_roots = new Array<i32>()
|
|
64
|
+
// Wrappers keep their existing request lifetime. Resource IDs are never traced
|
|
65
|
+
// as value handles; collection neither closes nor cancels a host resource.
|
|
66
|
+
@lazy const __pulse_handle_resource_roots = new Array<i32>()
|
|
67
|
+
let __pulse_handle_next: i32 = 0
|
|
68
|
+
let __pulse_handle_allocations: i32 = 0
|
|
69
|
+
let __pulse_handle_interval: i32 = 1024
|
|
70
|
+
let __pulse_handle_survivors: Map<i32, __PulseFastlyValue> | null = null
|
|
71
|
+
let __pulse_handle_work: Array<__PulseFastlyValue> | null = null
|
|
72
|
+
function __pulse_fastly_has_value(handle: i32): bool { return handle > 0 && __pulse_fastly_values.has(handle) }
|
|
73
|
+
function __pulse_fastly_value_count(): i32 { return __pulse_fastly_values.size }
|
|
74
|
+
function __pulse_fastly_put(value: __PulseFastlyValue): i32 {
|
|
75
|
+
// Provider-owned terminal error responses still allocate after a latched
|
|
76
|
+
// failure, just as on the retained path. Checkpoints remain suppressed.
|
|
77
|
+
if (__pulse_handle_next == i32.MAX_VALUE) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 1, -1); return 0 }
|
|
78
|
+
const handle = ++__pulse_handle_next
|
|
79
|
+
__pulse_fastly_values.set(handle, value)
|
|
80
|
+
if (value.kind == PULSE_VALUE_FETCH || value.s3Body || value.responseHandle != 0 || value.bodyHandle != 0) __pulse_handle_resource_roots.push(handle)
|
|
81
|
+
__pulse_handle_allocations++
|
|
82
|
+
return handle
|
|
83
|
+
}
|
|
84
|
+
function __pulse_fastly_value(handle: i32): __PulseFastlyValue {
|
|
85
|
+
if (!__pulse_fastly_has_value(handle)) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 1, -1); return new __PulseFastlyValue() }
|
|
86
|
+
return __pulse_fastly_values.get(handle)
|
|
87
|
+
}
|
|
88
|
+
function __pulse_handle_mark(handle: i32): void {
|
|
89
|
+
if (handle == 0 || __pulse_fastly_last_error != 0) return
|
|
90
|
+
const survivors = __pulse_handle_survivors!
|
|
91
|
+
if (survivors.has(handle)) return
|
|
92
|
+
if (!__pulse_fastly_has_value(handle)) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 1, -1); return }
|
|
93
|
+
const value = __pulse_fastly_values.get(handle)
|
|
94
|
+
survivors.set(handle, value)
|
|
95
|
+
__pulse_handle_work!.push(value)
|
|
96
|
+
}
|
|
97
|
+
@noinline
|
|
98
|
+
function __pulse_handle_checkpoint(): void {
|
|
99
|
+
if (__pulse_fastly_last_error != 0 || __pulse_error != 0 || __pulse_handle_allocations < __pulse_handle_interval) return
|
|
100
|
+
// Separate handle and object visitation: ropes retain direct objects, and
|
|
101
|
+
// multiple handle IDs can reference one object. Neither permits losing an ID.
|
|
102
|
+
__pulse_handle_survivors = new Map<i32, __PulseFastlyValue>()
|
|
103
|
+
__pulse_handle_work = new Array<__PulseFastlyValue>()
|
|
104
|
+
const visited = new Set<__PulseFastlyValue>()
|
|
105
|
+
__pulse_handle_guest_roots()
|
|
106
|
+
__pulse_handle_provider_roots()
|
|
107
|
+
const work = __pulse_handle_work!
|
|
108
|
+
while (work.length > 0 && __pulse_fastly_last_error == 0) {
|
|
109
|
+
const value = work.pop()
|
|
110
|
+
if (visited.has(value)) continue
|
|
111
|
+
visited.add(value)
|
|
112
|
+
value.traceHandleEdges()
|
|
113
|
+
}
|
|
114
|
+
// Publish only a complete trace. Rebuilding also drops historical Map
|
|
115
|
+
// insertion/deletion capacity; no append-only tombstone vector remains.
|
|
116
|
+
if (__pulse_fastly_last_error == 0) {
|
|
117
|
+
__pulse_fastly_values = __pulse_handle_survivors!
|
|
118
|
+
__pulse_handle_interval = max<i32>(1024, __pulse_fastly_values.size)
|
|
119
|
+
__pulse_handle_allocations = 0
|
|
120
|
+
}
|
|
121
|
+
__pulse_handle_survivors = null
|
|
122
|
+
__pulse_handle_work = null
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { fields, tableSource, validateRoots, providerRoots, providerSource };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function runtimeSource() {
|
|
4
|
+
return `
|
|
5
|
+
@external("fastly_http_resp", "header_values_get")
|
|
6
|
+
declare function __pulse_response_header_values(handle: i32, name: usize, length: i32, buffer: usize, size: i32, cursor: i32, end: usize, written: usize): i32
|
|
7
|
+
function __pulse_header_lookup_failed(): bool {
|
|
8
|
+
return __pulse_fastly_last_error == PULSE_ERROR_HOSTCALL && (__pulse_fastly_error_stage == 22 || __pulse_fastly_error_stage == 23);
|
|
9
|
+
}
|
|
10
|
+
function __pulse_header_lookup(handle: i32, name: string, response: bool): string | null {
|
|
11
|
+
const key = __pulse_fastly_utf8(name), buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES);
|
|
12
|
+
const end = new StaticArray<i64>(1), written = new StaticArray<i32>(1);
|
|
13
|
+
written[0] = -1;
|
|
14
|
+
// The single-value getter reports INVALID_ARGUMENT for absence on Viceroy.
|
|
15
|
+
// The list ABI distinguishes no values (zero bytes) from an empty value (NUL).
|
|
16
|
+
const status = response
|
|
17
|
+
? __pulse_response_header_values(handle, changetype<usize>(key), key.byteLength, buffer.dataStart, buffer.length, 0, changetype<usize>(end), changetype<usize>(written))
|
|
18
|
+
: __pulse_request_header_values(handle, changetype<usize>(key), key.byteLength, buffer.dataStart, buffer.length, 0, changetype<usize>(end), changetype<usize>(written));
|
|
19
|
+
const count = written[0], next = end[0];
|
|
20
|
+
if (status == FASTLY_STATUS_OK && count == 0 && (next == -1 || next == 0)) return null;
|
|
21
|
+
if (status != FASTLY_STATUS_OK || count <= 0 || count > buffer.length
|
|
22
|
+
|| (next != -1 && (next <= 0 || next > 4294967295)) || buffer[count - 1] != 0) {
|
|
23
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, response ? 23 : 22, -1); return null;
|
|
24
|
+
}
|
|
25
|
+
// Pulse's singular accessor returns the first value, including an empty one.
|
|
26
|
+
for (let i = 0; i < count; i++) {
|
|
27
|
+
if (buffer[i] == 0) return String.UTF8.decodeUnsafe(buffer.dataStart, i, false);
|
|
28
|
+
}
|
|
29
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, response ? 23 : 22, -1); return null;
|
|
30
|
+
}
|
|
31
|
+
function __pulse_header_send_error(): bool {
|
|
32
|
+
if (!__pulse_header_lookup_failed()) return false;
|
|
33
|
+
const output = new __PulseFastlyValue(); output.kind = PULSE_VALUE_RESPONSE;
|
|
34
|
+
output.status = 500; output.text = "Internal Server Error";
|
|
35
|
+
output.headers.push(new __PulseFastlyHeader("content-type", "text/plain; charset=utf-8"));
|
|
36
|
+
// Keep the original hostcall stage available to diagnostics.
|
|
37
|
+
__pulse_fastly_send_result(__pulse_fastly_put(output));
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
function instrument(source, lifecycle) {
|
|
43
|
+
source = source.replace(/function (host_\w+)\(([^)]*)\): (i32|void) \{/g,
|
|
44
|
+
(declaration, name, _args, result) => name === 'host_router_error_take' ? declaration
|
|
45
|
+
: `${declaration} if (__pulse_header_lookup_failed()) return${result === 'void' ? '' : ' 0'};`);
|
|
46
|
+
const marker = `function ${lifecycle?.legacyName('__pulse_fastly_jwt_send_error') || '__pulse_fastly_jwt_send_error'}(): void {`;
|
|
47
|
+
if (!source.includes(marker)) throw new Error('Fastly header-lookup error boundary drift.');
|
|
48
|
+
return source.replace(marker, marker + ' if (__pulse_header_send_error()) return;');
|
|
49
|
+
}
|
|
50
|
+
module.exports = {runtimeSource, instrument};
|