@pulse-compute/provider-fastly 1.0.0-beta.1 → 1.0.0-beta.10
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 +35 -2
- package/src/build/digest-native.js +25 -0
- package/src/build/effect-invocations.js +49 -0
- package/src/build/embedded-assets.as.ts +32 -0
- package/src/build/entities-native.js +6 -11
- package/src/build/kv-native.as.ts +331 -0
- package/src/build/kv-native.js +27 -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 +341 -0
- package/src/build/native-body-forwarding.js +69 -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-deadlines.as.ts +164 -0
- package/src/build/native-fetch-deadlines.js +27 -0
- package/src/build/native-fetch-errors.js +38 -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 +155 -32
- package/src/build/native-platform-capabilities.js +755 -210
- package/src/build/native-request-body.js +44 -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.as.ts +50 -0
- package/src/build/request-budget.js +47 -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/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 +7 -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 +22 -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 +9 -1
- package/src/testing/native-platform-capabilities-host.js +349 -24
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +16 -0
- package/src/toolchain/index.js +48 -5
- package/src/toolchain/logging.js +18 -0
- package/src/toolchain/native-execution.js +78 -0
- package/src/toolchain/s3.js +26 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Per-invocation, per-admission header deadlines. A reused effect site gets a new
|
|
2
|
+
// clock sample; waiting on an earlier group member cannot replenish this budget.
|
|
3
|
+
@external('fastly_async_io', 'select')
|
|
4
|
+
declare function __pulse_fetch_select(handles: usize, count: i32, timeout: i32, done: usize): i32
|
|
5
|
+
@external('fastly_http_req', 'pending_req_poll_v2')
|
|
6
|
+
declare function __pulse_fetch_poll(handle: i32, detail: usize, done: usize, response: usize, body: usize): i32
|
|
7
|
+
@external('fastly_http_resp', 'close')
|
|
8
|
+
declare function __pulse_fetch_close_response(handle: i32): i32
|
|
9
|
+
const __pulse_fetch_timeout = new StaticArray<f64>(PULSE_FASTLY_EFFECT_COUNT)
|
|
10
|
+
const __pulse_fetch_started = new StaticArray<i64>(PULSE_FASTLY_EFFECT_COUNT)
|
|
11
|
+
const __pulse_fetch_completed = new StaticArray<bool>(PULSE_FASTLY_EFFECT_COUNT)
|
|
12
|
+
// Zero = headers, positive = Fastly status, -1 = timeout, -2 = protocol/clock.
|
|
13
|
+
const __pulse_fetch_outcome = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
|
|
14
|
+
const __pulse_fetch_retired = new Array<i32>()
|
|
15
|
+
let __pulse_fetch_last_clock: i64 = -1
|
|
16
|
+
let __pulse_fetch_retired_total: i32 = 0
|
|
17
|
+
export function pulse_fastly_fetch_retired(): i32 { return __pulse_fetch_retired_total }
|
|
18
|
+
export function pulse_fastly_fetch_unreaped(): i32 { return __pulse_fetch_retired.length }
|
|
19
|
+
function __pulse_fetch_clock(index: i32): i64 {
|
|
20
|
+
const out = new StaticArray<i64>(1)
|
|
21
|
+
if (wasi_snapshot_preview1_clock_time_get(1, 1, changetype<usize>(out)) != 0
|
|
22
|
+
|| out[0] < 0 || out[0] < __pulse_fetch_last_clock) {
|
|
23
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); return -1
|
|
24
|
+
}
|
|
25
|
+
__pulse_fetch_last_clock = out[0]; return out[0]
|
|
26
|
+
}
|
|
27
|
+
function __pulse_fetch_begin_deadline(index: i32, init: __PulseFastlyValue): void {
|
|
28
|
+
__pulse_fetch_timeout[index] = 0; __pulse_fetch_completed[index] = false; __pulse_fetch_outcome[index] = 0
|
|
29
|
+
const field = __pulse_fastly_find(init, 'timeoutMs')
|
|
30
|
+
if (field < 0) return
|
|
31
|
+
const value = __pulse_fastly_value(unchecked(init.values[field]))
|
|
32
|
+
if (value.kind == PULSE_VALUE_UNDEFINED) return
|
|
33
|
+
const ms = value.number
|
|
34
|
+
if (value.kind != PULSE_VALUE_NUMBER || ms <= 0 || ms > 9007199254740991 || Math.floor(ms) != ms) {
|
|
35
|
+
__pulse_fastly_fail(PULSE_ERROR_VALUE, 270, index); return
|
|
36
|
+
}
|
|
37
|
+
__pulse_fetch_timeout[index] = ms
|
|
38
|
+
__pulse_fetch_started[index] = __pulse_fetch_clock(index)
|
|
39
|
+
}
|
|
40
|
+
function __pulse_fetch_remaining(index: i32, now: i64): f64 {
|
|
41
|
+
if (__pulse_fetch_timeout[index] == 0) return 2147483647.0
|
|
42
|
+
// Subtract elapsed nanoseconds before converting. Do not overflow by adding
|
|
43
|
+
// a safe-integer millisecond option to an i64 nanosecond clock.
|
|
44
|
+
return __pulse_fetch_timeout[index] - f64(now - __pulse_fetch_started[index]) / 1000000.0
|
|
45
|
+
}
|
|
46
|
+
function __pulse_fetch_before_send(index: i32): bool {
|
|
47
|
+
if (__pulse_fastly_effect_kind(index) != 1 || __pulse_fetch_timeout[index] == 0) return true
|
|
48
|
+
const now = __pulse_fetch_clock(index)
|
|
49
|
+
if (now < 0) return false
|
|
50
|
+
if (__pulse_fetch_remaining(index, now) > 0) return true
|
|
51
|
+
__pulse_fastly_fail(PULSE_ERROR_TRANSPORT, 271, index); return false
|
|
52
|
+
}
|
|
53
|
+
function __pulse_fetch_active(index: i32): bool {
|
|
54
|
+
return __pulse_fastly_effect_kind(index) == 1 && __pulse_fastly_pending_mode[index] == PULSE_FASTLY_PENDING_ASYNC
|
|
55
|
+
}
|
|
56
|
+
function __pulse_fetch_has_deadlines(): bool {
|
|
57
|
+
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++)
|
|
58
|
+
if (__pulse_fetch_active(i) && __pulse_fetch_timeout[i] > 0) return true
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
function __pulse_fetch_complete(index: i32, outcome: i32, result: i32 = 0): void {
|
|
62
|
+
__pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_READY
|
|
63
|
+
__pulse_fastly_pending[index] = 0; __pulse_fastly_ready[index] = result
|
|
64
|
+
__pulse_fetch_completed[index] = true; __pulse_fetch_outcome[index] = outcome
|
|
65
|
+
}
|
|
66
|
+
function __pulse_fetch_retire(index: i32, outcome: i32): void {
|
|
67
|
+
// The Fastly ABI has no pending-request cancellation hostcall. Retain explicit
|
|
68
|
+
// ownership until invocation cleanup; never wait, retry, or claim rollback.
|
|
69
|
+
__pulse_fetch_retired.push(__pulse_fastly_pending[index]); __pulse_fetch_retired_total++
|
|
70
|
+
__pulse_fetch_complete(index, outcome)
|
|
71
|
+
}
|
|
72
|
+
function __pulse_fetch_retire_all(): void {
|
|
73
|
+
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) if (__pulse_fetch_active(i)) __pulse_fetch_retire(i, -2)
|
|
74
|
+
}
|
|
75
|
+
function __pulse_fetch_reap(): void {
|
|
76
|
+
const detail = new StaticArray<i32>(__PULSE_SEND_ERROR_DETAIL_WORDS), done = new StaticArray<i32>(1)
|
|
77
|
+
const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1)
|
|
78
|
+
for (let i = __pulse_fetch_retired.length - 1; i >= 0; i--) {
|
|
79
|
+
done[0] = 0
|
|
80
|
+
__pulse_send_error_detail_reset(changetype<usize>(detail))
|
|
81
|
+
const status = __pulse_fetch_poll(__pulse_fetch_retired[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
|
|
82
|
+
if (status == 0 && done[0] == 0) continue
|
|
83
|
+
if (status == 0 && done[0] == 1) { __pulse_fetch_close_response(response[0]); __pulse_fetch_close_body(body[0]) }
|
|
84
|
+
__pulse_fetch_retired[i] = __pulse_fetch_retired[__pulse_fetch_retired.length - 1]; __pulse_fetch_retired.pop()
|
|
85
|
+
}
|
|
86
|
+
// Unready handles remain platform-owned until host teardown. No guest task
|
|
87
|
+
// survives _start and no response/body is exposed for an expired operation.
|
|
88
|
+
}
|
|
89
|
+
// Request-budget instrumentation replaces these hooks when maxDurationMs exists.
|
|
90
|
+
function __pulse_fetch_request_check(index: i32): bool { return true }
|
|
91
|
+
function __pulse_fetch_request_remaining(): i32 { return 2147483647 }
|
|
92
|
+
function __pulse_fetch_take(index: i32): i32 {
|
|
93
|
+
const outcome = __pulse_fetch_outcome[index], result = __pulse_fastly_ready[index]
|
|
94
|
+
__pulse_fetch_completed[index] = false; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_NONE
|
|
95
|
+
__pulse_fastly_ready[index] = 0
|
|
96
|
+
if (outcome == -1) __pulse_fastly_fail(PULSE_ERROR_TRANSPORT, 271, index)
|
|
97
|
+
else if (outcome == -2) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index)
|
|
98
|
+
else if (outcome != 0) __pulse_fetch_fail(outcome, 81, index)
|
|
99
|
+
return outcome == 0 ? result : 0
|
|
100
|
+
}
|
|
101
|
+
function __pulse_fetch_wait(index: i32): i32 {
|
|
102
|
+
const handles = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT), selected = new StaticArray<i32>(1)
|
|
103
|
+
const detail = new StaticArray<i32>(__PULSE_SEND_ERROR_DETAIL_WORDS), done = new StaticArray<i32>(1)
|
|
104
|
+
const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1)
|
|
105
|
+
while (!__pulse_fetch_completed[index]) {
|
|
106
|
+
if (!__pulse_fetch_request_check(index)) { __pulse_fetch_retire_all(); return 0 }
|
|
107
|
+
let now = __pulse_fetch_clock(index)
|
|
108
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
109
|
+
let count = 0, remaining = 2147483647.0
|
|
110
|
+
// Observe all dispatched fetches, not just the first static slot. Otherwise
|
|
111
|
+
// a slow earlier sibling could turn timely later headers into a false timeout.
|
|
112
|
+
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
|
|
113
|
+
if (!__pulse_fetch_active(i)) continue
|
|
114
|
+
if (__pulse_fetch_remaining(i, now) <= 0) { __pulse_fetch_retire(i, -1); continue }
|
|
115
|
+
done[0] = 0
|
|
116
|
+
__pulse_send_error_detail_reset(changetype<usize>(detail))
|
|
117
|
+
const status = __pulse_fetch_poll(__pulse_fastly_pending[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
|
|
118
|
+
now = __pulse_fetch_clock(i)
|
|
119
|
+
if (status != 0) {
|
|
120
|
+
__pulse_transport_capture(i, status, 'poll', changetype<usize>(detail))
|
|
121
|
+
__pulse_fetch_complete(i, now < 0 ? -2 : __pulse_fetch_remaining(i, now) <= 0 ? -1 : status)
|
|
122
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
if (done[0] != 0 && done[0] != 1) {
|
|
126
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, i); __pulse_fetch_retire_all(); return 0
|
|
127
|
+
}
|
|
128
|
+
if (now < 0 || __pulse_fetch_remaining(i, now) <= 0) {
|
|
129
|
+
if (done[0] == 1) {
|
|
130
|
+
__pulse_fetch_close_response(response[0]); __pulse_fetch_close_body(body[0]); __pulse_fetch_complete(i, now < 0 ? -2 : -1)
|
|
131
|
+
} else __pulse_fetch_retire(i, now < 0 ? -2 : -1)
|
|
132
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (done[0] == 1) {
|
|
136
|
+
const value = new __PulseFastlyValue(); value.kind = PULSE_VALUE_FETCH
|
|
137
|
+
value.responseHandle = response[0]; value.bodyHandle = body[0]
|
|
138
|
+
__pulse_fetch_complete(i, 0, __pulse_fastly_put(value)); continue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (__pulse_fetch_completed[index]) break
|
|
142
|
+
if (!__pulse_fetch_request_check(index)) { __pulse_fetch_retire_all(); return 0 }
|
|
143
|
+
now = __pulse_fetch_clock(index)
|
|
144
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
145
|
+
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
|
|
146
|
+
if (!__pulse_fetch_active(i)) continue
|
|
147
|
+
const budget = __pulse_fetch_remaining(i, now)
|
|
148
|
+
if (budget <= 0) { __pulse_fetch_retire(i, -1); continue }
|
|
149
|
+
handles[count++] = __pulse_fastly_pending[i]; remaining = Math.min(remaining, budget)
|
|
150
|
+
}
|
|
151
|
+
if (__pulse_fetch_completed[index]) break
|
|
152
|
+
remaining = Math.min(remaining, f64(__pulse_fetch_request_remaining()))
|
|
153
|
+
if (count == 0 || remaining <= 0) {
|
|
154
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); __pulse_fetch_retire_all(); return 0
|
|
155
|
+
}
|
|
156
|
+
selected[0] = -2
|
|
157
|
+
const status = __pulse_fetch_select(changetype<usize>(handles), count, i32(Math.ceil(remaining)), changetype<usize>(selected))
|
|
158
|
+
const after = __pulse_fetch_clock(index)
|
|
159
|
+
if (status != 0 || selected[0] < -1 || selected[0] >= count || after < 0 || (selected[0] == -1 && after <= now)) {
|
|
160
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); __pulse_fetch_retire_all(); return 0
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return __pulse_fetch_take(index)
|
|
164
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
function enabled(plan) { return (plan.effects || []).some(effect => effect.kind === 'fetch'); }
|
|
5
|
+
function runtimeSource(plan) {
|
|
6
|
+
if (!enabled(plan)) return `function __pulse_fetch_has_deadlines(): bool { return false }
|
|
7
|
+
function __pulse_fetch_request_check(index: i32): bool { return true }
|
|
8
|
+
function __pulse_fetch_request_remaining(): i32 { return 2147483647 }`;
|
|
9
|
+
return fs.readFileSync(path.join(__dirname,'native-fetch-deadlines.as.ts'),'utf8');
|
|
10
|
+
}
|
|
11
|
+
function instrument(input, plan) {
|
|
12
|
+
if (!enabled(plan)) return input;
|
|
13
|
+
let source = input;
|
|
14
|
+
const replace = (before,after) => {
|
|
15
|
+
if (!source.includes(before)) throw new Error('Fastly fetch deadline boundary drift: ' + before.slice(0,80));
|
|
16
|
+
source = source.replace(before,after);
|
|
17
|
+
};
|
|
18
|
+
replace('const init = initIndex >= 0 ? __pulse_fastly_value(unchecked(payload.values[initIndex])) : new __PulseFastlyValue(); let method',
|
|
19
|
+
'const init = initIndex >= 0 ? __pulse_fastly_value(unchecked(payload.values[initIndex])) : new __PulseFastlyValue(); __pulse_fetch_begin_deadline(effectIndex, init); if (__pulse_fastly_last_error != 0) return; let method');
|
|
20
|
+
replace('const backendBytes = __pulse_fastly_utf8(backend); const pendingOut',
|
|
21
|
+
'if (!__pulse_fetch_before_send(effectIndex)) { __pulse_fetch_close_request(requestHandle); __pulse_fetch_close_body(bodyHandle); return; } const backendBytes = __pulse_fastly_utf8(backend); const pendingOut');
|
|
22
|
+
replace('function __pulse_fastly_wait_fetch(effectIndex: i32): i32 {',
|
|
23
|
+
'function __pulse_fastly_wait_fetch(effectIndex: i32): i32 { if (__pulse_fastly_effect_kind(effectIndex) == 1 && (__pulse_fetch_completed[effectIndex] || __pulse_fetch_has_deadlines())) return __pulse_fetch_wait(effectIndex);');
|
|
24
|
+
replace('function __pulse_invocation_close(): void {', 'function __pulse_invocation_close(): void { __pulse_fetch_reap();');
|
|
25
|
+
return source;
|
|
26
|
+
}
|
|
27
|
+
module.exports = {runtimeSource,instrument};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function runtimeSource(dynamic = false) {
|
|
4
|
+
return `
|
|
5
|
+
@external("fastly_http_req", "close")
|
|
6
|
+
declare function __pulse_fetch_close_request(handle: i32): i32
|
|
7
|
+
@external("fastly_http_body", "close")
|
|
8
|
+
declare function __pulse_fetch_close_body(handle: i32): i32
|
|
9
|
+
function __pulse_fetch_fail(status: i32, stage: i32, effect: i32): void {
|
|
10
|
+
// Fastly ERROR and HTTP parse/user/incomplete/size/status errors represent
|
|
11
|
+
// failed exchanges. Invalid arguments, handles and unsupported ABI stay fatal.
|
|
12
|
+
const network = __pulse_fastly_effect_kind(effect) != 1 || status == 1 || status == 7 || status == 8 || status == 9 || status == 11 || status == 12;
|
|
13
|
+
if (__pulse_fastly_last_error == 0) __pulse_transport_error = __pulse_transport_slots[effect];
|
|
14
|
+
__pulse_fastly_fail(network ? PULSE_ERROR_TRANSPORT : PULSE_ERROR_HOSTCALL, stage, effect);
|
|
15
|
+
}
|
|
16
|
+
function __pulse_fetch_failed(): bool {
|
|
17
|
+
return (__pulse_fastly_last_error == PULSE_ERROR_TRANSPORT || __pulse_fastly_last_error == PULSE_ERROR_HOSTCALL || __pulse_fastly_last_error == PULSE_ERROR_VALUE)
|
|
18
|
+
&& (__pulse_fastly_error_stage == 77 || __pulse_fastly_error_stage == 81 || (__pulse_fastly_error_stage >= 270 && __pulse_fastly_error_stage <= ${dynamic ? 276 : 272}))
|
|
19
|
+
&& __pulse_fastly_effect_kind(__pulse_fastly_error_effect) == 1;
|
|
20
|
+
}
|
|
21
|
+
function __pulse_fetch_send_error(): bool {
|
|
22
|
+
if (!__pulse_fetch_failed()) return false;
|
|
23
|
+
const output = new __PulseFastlyValue(); output.kind = PULSE_VALUE_RESPONSE;
|
|
24
|
+
const network = __pulse_fastly_last_error == PULSE_ERROR_TRANSPORT;
|
|
25
|
+
output.status = ${dynamic ? '__pulse_fastly_error_stage == 273 ? 400 : ' : ''}__pulse_fastly_error_stage == 270 ? 400 : __pulse_fastly_error_stage == 271 ? 504 : network ? 502 : 500;
|
|
26
|
+
output.text = ${dynamic ? '__pulse_fastly_error_stage == 273 ? "Unsupported or invalid Native dynamic URL" : __pulse_fastly_error_stage == 274 ? "Dynamic Backends unavailable; enable the Fastly service capability" : __pulse_fastly_error_stage == 275 ? "Dynamic backend registration failed; check service configuration" : __pulse_fastly_error_stage == 276 ? "Dynamic backend registration attempt limit exceeded" : ' : ''}__pulse_fastly_error_stage == 270 ? "Invalid fetch timeoutMs" : __pulse_fastly_error_stage == 271 ? "Gateway Timeout" : network ? "Bad Gateway" : "Internal Server Error";
|
|
27
|
+
output.headers.push(new __PulseFastlyHeader("content-type", "text/plain; charset=utf-8"));
|
|
28
|
+
__pulse_fastly_send_result(__pulse_fastly_put(output));
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
function instrument(source) {
|
|
34
|
+
const marker = 'function __pulse_fastly_jwt_send_error(): void {';
|
|
35
|
+
if (!source.includes(marker)) throw new Error('Fastly fetch error boundary drift.');
|
|
36
|
+
return source.replace(marker, marker + ' if (__pulse_fetch_send_error()) return;');
|
|
37
|
+
}
|
|
38
|
+
module.exports = {runtimeSource, instrument};
|
|
@@ -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) {
|
|
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 __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};
|