@pulse-compute/provider-fastly 1.0.0-beta.7 → 1.0.0-beta.9
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 +6 -6
- package/dist/config-schema.json +13 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/build/canonical-target.js +7 -0
- package/src/build/entities-native.js +6 -13
- package/src/build/kv-native.as.ts +10 -6
- package/src/build/native-application-errors.js +14 -8
- package/src/build/native-body-forwarding.as.ts +334 -0
- package/src/build/native-body-forwarding.js +69 -0
- package/src/build/native-fetch-deadlines.as.ts +161 -0
- package/src/build/native-fetch-deadlines.js +27 -0
- package/src/build/native-fetch-errors.js +40 -0
- package/src/build/native-handle-ownership.js +127 -0
- package/src/build/native-header-lookup.js +54 -0
- package/src/build/native-http-effects.js +6 -1
- package/src/build/native-platform-capabilities.js +99 -33
- package/src/build/native-stdio.js +20 -0
- package/src/build/native-string-values.js +2 -2
- package/src/build/native-value-budget.js +33 -1
- package/src/build/request-budget.js +3 -1
- package/src/build/schema-admission.js +3 -3
- package/src/config-api.js +1 -0
- package/src/config-schema.json +13 -0
- package/src/index.ts +11 -0
- package/src/provider-contract.js +6 -3
- package/src/testing/native-platform-capabilities-host.js +139 -24
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +4 -0
- package/src/toolchain/index.js +4 -2
- package/src/toolchain/native-execution.js +7 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// FWD9-02: sequential upload/response pumps. Payload bytes never become Pulse
|
|
2
|
+
// values. Fixed scratch and ABI outputs are reused, including the clock slot.
|
|
3
|
+
// close accepts EOF into Fastly; it is not a delivery/drain receipt.
|
|
4
|
+
@external('fastly_http_req', 'send_async_streaming')
|
|
5
|
+
declare function __fwd_dispatch(request: i32, body: i32, backend: usize, length: i32, pending: usize): i32
|
|
6
|
+
@external('fastly_http_body', 'abandon') declare function __fwd_abandon(body: i32): i32
|
|
7
|
+
@external('fastly_async_io', 'is_ready') declare function __fwd_ready(handle: i32, ready: usize): i32
|
|
8
|
+
@external('fastly_http_resp', 'header_names_get')
|
|
9
|
+
declare function __fwd_names(handle: i32, buffer: usize, size: i32, cursor: i32, end: usize, written: usize): i32
|
|
10
|
+
@external('fastly_http_resp', 'header_values_get')
|
|
11
|
+
declare function __fwd_values(handle: i32, name: usize, length: i32, buffer: usize, size: i32, cursor: i32, end: usize, written: usize): i32
|
|
12
|
+
const __fwd_buffer: usize = memory.data(16384)
|
|
13
|
+
const __fwd_headers: usize = memory.data(16384)
|
|
14
|
+
const __fwd_pairs: usize = memory.data(1024) // 64 x (name ptr/size, value ptr/size)
|
|
15
|
+
const __fwd_io: usize = memory.data(128) // aligned reusable ABI outputs
|
|
16
|
+
let __fwd_source: i32 = -1
|
|
17
|
+
let __fwd_upload: i32 = -1
|
|
18
|
+
let __fwd_request: i32 = -1
|
|
19
|
+
let __fwd_pending: i32 = -1
|
|
20
|
+
let __fwd_response: i32 = -1
|
|
21
|
+
let __fwd_body: i32 = -1
|
|
22
|
+
let __fwd_downstream: i32 = -1
|
|
23
|
+
let __fwd_output: i32 = -1
|
|
24
|
+
let __fwd_index: i32 = -1
|
|
25
|
+
let __fwd_result: i32 = 0
|
|
26
|
+
let __fwd_marker: i32 = 0
|
|
27
|
+
let __fwd_failure: i32 = 0
|
|
28
|
+
let __fwd_committed: bool = false
|
|
29
|
+
let __fwd_reserved: bool = false
|
|
30
|
+
let __fwd_header_count: i32 = 0
|
|
31
|
+
let __fwd_request_length: i32 = -1
|
|
32
|
+
let __fwd_response_length: i32 = -1
|
|
33
|
+
let __fwd_status: i32 = 200
|
|
34
|
+
function host_request_body(): i32 {
|
|
35
|
+
if (__fwd_marker != 0 || __fwd_reserved) { __fwd_fail(400); return 0; }
|
|
36
|
+
const marker = new __PulseFastlyValue(); marker.kind = 90;
|
|
37
|
+
__fwd_marker = __pulse_fastly_put(marker); return __fwd_marker;
|
|
38
|
+
}
|
|
39
|
+
function __fwd_fail(status: i32): void {
|
|
40
|
+
if (__fwd_failure == 0) __fwd_failure = status;
|
|
41
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 280, __fwd_index);
|
|
42
|
+
}
|
|
43
|
+
function __fwd_remaining(): i32 {
|
|
44
|
+
if (__fwd_failure != 0) return 0;
|
|
45
|
+
if (wasi_snapshot_preview1_clock_time_get(1, 1, __fwd_io) != 0) { __fwd_fail(504); return 0; }
|
|
46
|
+
const now = load<i64>(__fwd_io);
|
|
47
|
+
if (now < 0 || now < __request_last_clock || now < __pulse_fetch_last_clock) { __fwd_fail(504); return 0; }
|
|
48
|
+
__request_last_clock = now; __pulse_fetch_last_clock = now;
|
|
49
|
+
let remaining = f64(__request_deadline - now) / 1000000.0;
|
|
50
|
+
if (__request_expired || remaining <= 0) { __request_expired = true; __fwd_fail(504); return 0; }
|
|
51
|
+
if (__fwd_index >= 0) remaining = Math.min(remaining, __pulse_fetch_remaining(__fwd_index, now));
|
|
52
|
+
if (remaining <= 0) { __fwd_fail(504); return 0; }
|
|
53
|
+
return i32(Math.ceil(remaining)); // 1..30000; zero never reaches async.select
|
|
54
|
+
}
|
|
55
|
+
function __fwd_cleanup(): void {
|
|
56
|
+
if (__fwd_source >= 0) { __pulse_fetch_close_body(__fwd_source); __fwd_source = -1; }
|
|
57
|
+
if (__fwd_upload >= 0) { __fwd_abandon(__fwd_upload); __fwd_upload = -1; }
|
|
58
|
+
if (__fwd_request >= 0) { __pulse_fetch_close_request(__fwd_request); __fwd_request = -1; }
|
|
59
|
+
if (__fwd_body >= 0) { __pulse_fetch_close_body(__fwd_body); __fwd_body = -1; }
|
|
60
|
+
if (__fwd_response >= 0) { __pulse_fetch_close_response(__fwd_response); __fwd_response = -1; }
|
|
61
|
+
if (__fwd_output >= 0) { __pulse_fetch_close_response(__fwd_output); __fwd_output = -1; }
|
|
62
|
+
if (__fwd_downstream >= 0) { __fwd_abandon(__fwd_downstream); __fwd_downstream = -1; }
|
|
63
|
+
if (__fwd_pending >= 0) {
|
|
64
|
+
// No pending-request cancel ABI. Poll once without waiting and discard any
|
|
65
|
+
// late handles; an unresolved host operation ends at invocation teardown.
|
|
66
|
+
store<i32>(__fwd_io + 24, 0);
|
|
67
|
+
const status = __pulse_fetch_poll(__fwd_pending, __fwd_io + 8, __fwd_io + 24, __fwd_io + 28, __fwd_io + 32);
|
|
68
|
+
if (status == 0 && load<i32>(__fwd_io + 24) == 1) {
|
|
69
|
+
__pulse_fetch_close_response(load<i32>(__fwd_io + 28)); __pulse_fetch_close_body(load<i32>(__fwd_io + 32));
|
|
70
|
+
}
|
|
71
|
+
__fwd_pending = -1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function __fwd_error_response(): bool {
|
|
75
|
+
if (__fwd_committed) { __fwd_cleanup(); return true; }
|
|
76
|
+
if (__fwd_failure == 0) return false;
|
|
77
|
+
const status = __fwd_failure;
|
|
78
|
+
__fwd_cleanup(); __request_error_response = true;
|
|
79
|
+
__pulse_fastly_last_error = 0;
|
|
80
|
+
const value = new __PulseFastlyValue(); value.kind = PULSE_VALUE_RESPONSE; value.status = status;
|
|
81
|
+
value.text = status == 413 ? 'Payload Too Large' : status == 504 ? 'Gateway Timeout' : status == 400 ? 'Bad Request' : 'Bad Gateway';
|
|
82
|
+
__fwd_committed = true;
|
|
83
|
+
__pulse_fastly_send_result(__pulse_fastly_put(value)); return true;
|
|
84
|
+
}
|
|
85
|
+
function __fwd_lower(byte: u8): u8 { return byte >= 65 && byte <= 90 ? byte + 32 : byte; }
|
|
86
|
+
function __fwd_equal(ptr: usize, size: i32, text: string): bool {
|
|
87
|
+
if (size != text.length) return false;
|
|
88
|
+
for (let i = 0; i < size; i++) if (__fwd_lower(load<u8>(ptr + i)) != text.charCodeAt(i)) return false;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
function __fwd_token(byte: u8): bool {
|
|
92
|
+
return (byte >= 48 && byte <= 57) || (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)
|
|
93
|
+
|| byte == 33 || byte == 35 || byte == 36 || byte == 37 || byte == 38 || byte == 39 || byte == 42
|
|
94
|
+
|| byte == 43 || byte == 45 || byte == 46 || byte == 94 || byte == 95 || byte == 96 || byte == 124 || byte == 126;
|
|
95
|
+
}
|
|
96
|
+
function __fwd_hop(ptr: usize, size: i32): bool {
|
|
97
|
+
return __fwd_equal(ptr,size,'connection') || __fwd_equal(ptr,size,'keep-alive') || __fwd_equal(ptr,size,'proxy-connection')
|
|
98
|
+
|| __fwd_equal(ptr,size,'transfer-encoding') || __fwd_equal(ptr,size,'te') || __fwd_equal(ptr,size,'trailer')
|
|
99
|
+
|| __fwd_equal(ptr,size,'upgrade') || __fwd_equal(ptr,size,'content-length') || __fwd_equal(ptr,size,'host')
|
|
100
|
+
|| __fwd_equal(ptr,size,'proxy-authenticate') || __fwd_equal(ptr,size,'proxy-authorization');
|
|
101
|
+
}
|
|
102
|
+
// Decimal parsing stops at cap+1: huge declared sizes cannot overflow or wrap.
|
|
103
|
+
function __fwd_length(ptr: usize, size: i32, cap: i32): i32 {
|
|
104
|
+
if (size == 0) return -2;
|
|
105
|
+
let value = 0;
|
|
106
|
+
for (let i = 0; i < size; i++) {
|
|
107
|
+
const byte = load<u8>(ptr + i); if (byte < 48 || byte > 57) return -2;
|
|
108
|
+
value = min(cap + 1, value * 10 + byte - 48);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function __fwd_declared_request(): bool {
|
|
113
|
+
const name = memory.data<u8>([99,111,110,116,101,110,116,45,108,101,110,103,116,104]);
|
|
114
|
+
store<i64>(__fwd_io + 40, -1); store<i32>(__fwd_io + 48, 0);
|
|
115
|
+
const status = __pulse_request_header_values(__pulse_fastly_request_handle, name, 14, __fwd_headers, 16384, 0, __fwd_io + 40, __fwd_io + 48);
|
|
116
|
+
const count = load<i32>(__fwd_io + 48);
|
|
117
|
+
if (status != 0 || count < 0 || count > 16384 || (count > 0 && load<i64>(__fwd_io + 40) != -1)) { __fwd_fail(400); return false; }
|
|
118
|
+
if (count == 0) return true;
|
|
119
|
+
if (load<u8>(__fwd_headers + count - 1) != 0) { __fwd_fail(400); return false; }
|
|
120
|
+
__fwd_request_length = __fwd_length(__fwd_headers, count - 1, __FWD_REQUEST_MAX);
|
|
121
|
+
if (__fwd_request_length < 0) { __fwd_fail(400); return false; }
|
|
122
|
+
if (__fwd_request_length > __FWD_REQUEST_MAX) { __fwd_fail(413); return false; }
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
function __fwd_begin(index: i32, payload: __PulseFastlyValue): void {
|
|
126
|
+
__fwd_index = index;
|
|
127
|
+
const init = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'init'));
|
|
128
|
+
if (__fwd_reserved || __fwd_source < 0 || __pulse_fastly_payload_field(init, 'body') != __fwd_marker || __fwd_marker == 0) { __fwd_fail(400); return; }
|
|
129
|
+
__fwd_reserved = true;
|
|
130
|
+
__pulse_fetch_begin_deadline(index, init);
|
|
131
|
+
if (__pulse_fastly_last_error != 0 || !__fwd_remaining() || !__fwd_declared_request()) return;
|
|
132
|
+
const url = __pulse_fastly_utf8(__pulse_fastly_string(__pulse_fastly_payload_field(payload, 'url')));
|
|
133
|
+
const backend = __pulse_fastly_utf8(__pulse_fastly_backend(index));
|
|
134
|
+
if (fastly_http_req_new(__fwd_io + 52) != 0) { __fwd_fail(502); return; }
|
|
135
|
+
__fwd_request = load<i32>(__fwd_io + 52);
|
|
136
|
+
if (fastly_http_body_new(__fwd_io + 52) != 0) { __fwd_fail(502); return; }
|
|
137
|
+
__fwd_upload = load<i32>(__fwd_io + 52);
|
|
138
|
+
const method = memory.data<u8>([80,79,83,84]);
|
|
139
|
+
if (fastly_http_req_method_set(__fwd_request, method, 4) != 0 || fastly_http_req_uri_set(__fwd_request, changetype<usize>(url), url.byteLength) != 0) { __fwd_fail(502); return; }
|
|
140
|
+
const headers = __pulse_fastly_value(__pulse_fastly_payload_field(init, 'headers'));
|
|
141
|
+
let bytes = 0;
|
|
142
|
+
if (headers.kind == PULSE_VALUE_OBJECT) for (let i = 0; i < headers.keys.length; i++) {
|
|
143
|
+
const name = __pulse_fastly_utf8(headers.keys[i]), value = __pulse_fastly_utf8(__pulse_fastly_string(headers.values[i]));
|
|
144
|
+
bytes += name.byteLength + value.byteLength + 4;
|
|
145
|
+
if (i >= 64 || bytes > 16384 || __fwd_hop(changetype<usize>(name), name.byteLength)) { __fwd_fail(400); return; }
|
|
146
|
+
if (fastly_http_req_header_insert(__fwd_request, changetype<usize>(name), name.byteLength, changetype<usize>(value), value.byteLength) != 0) { __fwd_fail(400); return; }
|
|
147
|
+
}
|
|
148
|
+
if (!__fwd_remaining()) return;
|
|
149
|
+
const status = __fwd_dispatch(__fwd_request, __fwd_upload, changetype<usize>(backend), backend.byteLength, __fwd_io + 52);
|
|
150
|
+
// send may consume the request even on failure. Cleanup makes only a best-effort
|
|
151
|
+
// close on that error path before any handle allocation/reuse can occur.
|
|
152
|
+
if (status != 0) { __fwd_fail(502); return; }
|
|
153
|
+
__fwd_request = -1; __fwd_pending = load<i32>(__fwd_io + 52);
|
|
154
|
+
__pulse_fastly_pending[index] = __fwd_pending; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_ASYNC;
|
|
155
|
+
__fwd_remaining();
|
|
156
|
+
}
|
|
157
|
+
// Adopt early headers before considering readiness of the source or writer.
|
|
158
|
+
function __fwd_poll(): bool {
|
|
159
|
+
if (__fwd_pending < 0 || !__fwd_remaining()) return __fwd_response >= 0;
|
|
160
|
+
store<i32>(__fwd_io + 24, 0);
|
|
161
|
+
const status = __pulse_fetch_poll(__fwd_pending, __fwd_io + 8, __fwd_io + 24, __fwd_io + 28, __fwd_io + 32);
|
|
162
|
+
const done = load<i32>(__fwd_io + 24);
|
|
163
|
+
if (status != 0) { __fwd_pending = -1; __fwd_fail(502); return false; }
|
|
164
|
+
if (done != 0 && done != 1) { __fwd_fail(502); return false; }
|
|
165
|
+
if (done == 1) {
|
|
166
|
+
__fwd_response = load<i32>(__fwd_io + 28); __fwd_body = load<i32>(__fwd_io + 32); __fwd_pending = -1;
|
|
167
|
+
}
|
|
168
|
+
if (!__fwd_remaining()) return false;
|
|
169
|
+
return done == 1;
|
|
170
|
+
}
|
|
171
|
+
function __fwd_wait_io(handle: i32, upload: bool): bool {
|
|
172
|
+
while (__fwd_remaining() > 0) {
|
|
173
|
+
if (upload && __fwd_poll()) return false;
|
|
174
|
+
if (__fwd_failure != 0) return false;
|
|
175
|
+
store<i32>(__fwd_io + 52, -1);
|
|
176
|
+
if (__fwd_ready(handle, __fwd_io + 52) != 0) { __fwd_fail(502); return false; }
|
|
177
|
+
const ready = load<i32>(__fwd_io + 52);
|
|
178
|
+
if (ready == 1) return __fwd_remaining() > 0;
|
|
179
|
+
if (ready != 0) { __fwd_fail(502); return false; }
|
|
180
|
+
let count = 0;
|
|
181
|
+
if (upload && __fwd_pending >= 0) { store<i32>(__fwd_io + 56, __fwd_pending); count++; }
|
|
182
|
+
store<i32>(__fwd_io + 56 + count * 4, handle); count++;
|
|
183
|
+
const remaining = __fwd_remaining(); if (remaining == 0) return false;
|
|
184
|
+
const status = __pulse_fetch_select(__fwd_io + 56, count, remaining, __fwd_io + 64);
|
|
185
|
+
if (status != 0) { __fwd_fail(502); return false; }
|
|
186
|
+
const selected = load<i32>(__fwd_io + 64);
|
|
187
|
+
if (selected == -1) { __fwd_fail(504); return false; }
|
|
188
|
+
if (selected < 0 || selected >= count) { __fwd_fail(502); return false; }
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
function __fwd_copy(source: i32, sink: i32, cap: i32, declared: i32, upload: bool): bool {
|
|
193
|
+
let total = 0;
|
|
194
|
+
while (true) {
|
|
195
|
+
// Demand first; at most one raw chunk can be read ahead of the writer.
|
|
196
|
+
if (!__fwd_wait_io(sink, upload) || !__fwd_wait_io(source, upload)) return false;
|
|
197
|
+
const size = min(16384, cap - total + 1);
|
|
198
|
+
store<i32>(__fwd_io + 68, -1);
|
|
199
|
+
if (fastly_http_body_read(source, __fwd_buffer, size, __fwd_io + 68) != 0) { __fwd_fail(upload ? 400 : 502); return false; }
|
|
200
|
+
const read = load<i32>(__fwd_io + 68);
|
|
201
|
+
if (!__fwd_remaining()) return false;
|
|
202
|
+
if (read < 0 || read > size) { __fwd_fail(502); return false; }
|
|
203
|
+
if (read == 0) {
|
|
204
|
+
if (declared >= 0 && total != declared) { __fwd_fail(upload ? 400 : 502); return false; }
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
total += read;
|
|
208
|
+
if (total > cap) { __fwd_fail(upload ? 413 : 502); return false; }
|
|
209
|
+
if (declared >= 0 && total > declared) { __fwd_fail(upload ? 400 : 502); return false; }
|
|
210
|
+
let offset = 0;
|
|
211
|
+
while (offset < read) {
|
|
212
|
+
if (!__fwd_wait_io(sink, upload)) return false;
|
|
213
|
+
store<i32>(__fwd_io + 72, -1);
|
|
214
|
+
if (fastly_http_body_write(sink, __fwd_buffer + offset, read - offset, 0, __fwd_io + 72) != 0) { __fwd_fail(502); return false; }
|
|
215
|
+
const written = load<i32>(__fwd_io + 72);
|
|
216
|
+
if (!__fwd_remaining()) return false;
|
|
217
|
+
if (written <= 0 || written > read - offset) { __fwd_fail(502); return false; }
|
|
218
|
+
offset += written;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function __fwd_wait(): i32 {
|
|
223
|
+
if (__fwd_failure != 0 || __fwd_pending < 0) return 0;
|
|
224
|
+
const eof = __fwd_copy(__fwd_source, __fwd_upload, __FWD_REQUEST_MAX, __fwd_request_length, true);
|
|
225
|
+
__pulse_fetch_close_body(__fwd_source); __fwd_source = -1;
|
|
226
|
+
if (eof && __fwd_remaining()) {
|
|
227
|
+
const upload = __fwd_upload; __fwd_upload = -1;
|
|
228
|
+
if (__pulse_fetch_close_body(upload) != 0) __fwd_fail(502);
|
|
229
|
+
} else if (__fwd_upload >= 0) { __fwd_abandon(__fwd_upload); __fwd_upload = -1; }
|
|
230
|
+
while (__fwd_failure == 0 && __fwd_response < 0) {
|
|
231
|
+
if (__fwd_poll()) break;
|
|
232
|
+
if (__fwd_failure != 0 || !__fwd_wait_io(__fwd_pending, false)) break;
|
|
233
|
+
}
|
|
234
|
+
__pulse_fastly_pending_mode[__fwd_index] = PULSE_FASTLY_PENDING_NONE; __pulse_fastly_pending[__fwd_index] = 0;
|
|
235
|
+
if (__fwd_failure != 0 || !__fwd_remaining()) return 0;
|
|
236
|
+
const value = new __PulseFastlyValue(); value.kind = PULSE_VALUE_FETCH;
|
|
237
|
+
value.responseHandle = __fwd_response; value.bodyHandle = __fwd_body;
|
|
238
|
+
__fwd_result = __pulse_fastly_put(value); return __fwd_result;
|
|
239
|
+
}
|
|
240
|
+
function __fwd_response_headers(): bool {
|
|
241
|
+
store<i32>(__fwd_io + 52, 0);
|
|
242
|
+
if (fastly_http_resp_status_get(__fwd_response, __fwd_io + 52) != 0) { __fwd_fail(502); return false; }
|
|
243
|
+
__fwd_status = load<u16>(__fwd_io + 52);
|
|
244
|
+
if (__fwd_status < 200 || __fwd_status > 599) { __fwd_fail(502); return false; }
|
|
245
|
+
store<i64>(__fwd_io + 40, -1); store<i32>(__fwd_io + 48, -1);
|
|
246
|
+
if (__fwd_names(__fwd_response, __fwd_headers, 16384, 0, __fwd_io + 40, __fwd_io + 48) != 0) { __fwd_fail(502); return false; }
|
|
247
|
+
const names = load<i32>(__fwd_io + 48);
|
|
248
|
+
if (names < 0 || names > 16384 || (names > 0 && load<i64>(__fwd_io + 40) != -1)) { __fwd_fail(502); return false; }
|
|
249
|
+
let used = names, start = 0, total = 0, nameCount = 0;
|
|
250
|
+
for (let i = 0; i < names; i++) {
|
|
251
|
+
const byte = load<u8>(__fwd_headers + i);
|
|
252
|
+
if (byte != 0) { if (!__fwd_token(byte)) { __fwd_fail(502); return false; } continue; }
|
|
253
|
+
const name = __fwd_headers + start, length = i - start; start = i + 1;
|
|
254
|
+
if (length == 0 || ++nameCount > 64 || used >= 16384) { __fwd_fail(502); return false; }
|
|
255
|
+
store<i64>(__fwd_io + 40, -1); store<i32>(__fwd_io + 48, -1);
|
|
256
|
+
if (__fwd_values(__fwd_response, name, length, __fwd_headers + used, 16384 - used, 0, __fwd_io + 40, __fwd_io + 48) != 0) { __fwd_fail(502); return false; }
|
|
257
|
+
const size = load<i32>(__fwd_io + 48);
|
|
258
|
+
if (size <= 0 || size > 16384 - used || load<i64>(__fwd_io + 40) != -1) { __fwd_fail(502); return false; }
|
|
259
|
+
let valueStart = 0;
|
|
260
|
+
for (let j = 0; j < size; j++) {
|
|
261
|
+
const item = load<u8>(__fwd_headers + used + j);
|
|
262
|
+
if (item != 0) { if ((item < 32 && item != 9) || item == 127) { __fwd_fail(502); return false; } continue; }
|
|
263
|
+
const value = __fwd_headers + used + valueStart, valueSize = j - valueStart; valueStart = j + 1;
|
|
264
|
+
total += length + valueSize + 4;
|
|
265
|
+
if (__fwd_header_count >= 64 || total > 16384) { __fwd_fail(502); return false; }
|
|
266
|
+
const pair = __fwd_pairs + __fwd_header_count * 16; __fwd_header_count++;
|
|
267
|
+
store<usize>(pair, name); store<i32>(pair + 4, length); store<usize>(pair + 8, value); store<i32>(pair + 12, valueSize);
|
|
268
|
+
if (__fwd_equal(name, length, 'content-length')) {
|
|
269
|
+
if (__fwd_response_length != -1) { __fwd_fail(502); return false; }
|
|
270
|
+
__fwd_response_length = __fwd_length(value, valueSize, __FWD_RESPONSE_MAX);
|
|
271
|
+
if (__fwd_response_length < 0 || (__fwd_status != 304 && __fwd_response_length > __FWD_RESPONSE_MAX)) { __fwd_fail(502); return false; }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (valueStart != size) { __fwd_fail(502); return false; }
|
|
275
|
+
used += size;
|
|
276
|
+
}
|
|
277
|
+
if (start != names || ((__fwd_status == 204 || __fwd_status == 205) && __fwd_response_length > 0)) { __fwd_fail(502); return false; }
|
|
278
|
+
return __fwd_remaining() > 0;
|
|
279
|
+
}
|
|
280
|
+
function __fwd_nominated(name: usize, length: i32): bool {
|
|
281
|
+
let nominated = false;
|
|
282
|
+
for (let i = 0; i < __fwd_header_count; i++) {
|
|
283
|
+
const pair = __fwd_pairs + i * 16;
|
|
284
|
+
if (!__fwd_equal(load<usize>(pair), load<i32>(pair + 4), 'connection')) continue;
|
|
285
|
+
const value = load<usize>(pair + 8), size = load<i32>(pair + 12);
|
|
286
|
+
let offset = 0;
|
|
287
|
+
while (offset < size) {
|
|
288
|
+
while (offset < size && (load<u8>(value + offset) == 32 || load<u8>(value + offset) == 9)) offset++;
|
|
289
|
+
const start = offset;
|
|
290
|
+
while (offset < size && __fwd_token(load<u8>(value + offset))) offset++;
|
|
291
|
+
const count = offset - start;
|
|
292
|
+
if (count == 0) { __fwd_fail(502); return false; }
|
|
293
|
+
if (count == length) {
|
|
294
|
+
let same = true;
|
|
295
|
+
for (let c = 0; c < count; c++) if (__fwd_lower(load<u8>(value + start + c)) != __fwd_lower(load<u8>(name + c))) same = false;
|
|
296
|
+
if (same) nominated = true;
|
|
297
|
+
}
|
|
298
|
+
while (offset < size && (load<u8>(value + offset) == 32 || load<u8>(value + offset) == 9)) offset++;
|
|
299
|
+
if (offset < size && (load<u8>(value + offset++) != 44 || offset == size)) { __fwd_fail(502); return false; }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return nominated;
|
|
303
|
+
}
|
|
304
|
+
function __fwd_send(): i32 {
|
|
305
|
+
if (!__fwd_response_headers()) { __fwd_error_response(); return 0; }
|
|
306
|
+
if (fastly_http_resp_new(__fwd_io + 52) != 0) { __fwd_fail(502); __fwd_error_response(); return 0; }
|
|
307
|
+
__fwd_output = load<i32>(__fwd_io + 52);
|
|
308
|
+
if (fastly_http_resp_status_set(__fwd_output, __fwd_status) != 0 || fastly_http_body_new(__fwd_io + 52) != 0) { __fwd_fail(502); __fwd_error_response(); return 0; }
|
|
309
|
+
__fwd_downstream = load<i32>(__fwd_io + 52);
|
|
310
|
+
for (let i = 0; i < __fwd_header_count; i++) {
|
|
311
|
+
const pair = __fwd_pairs + i * 16, name = load<usize>(pair), length = load<i32>(pair + 4);
|
|
312
|
+
const nominated = __fwd_nominated(name, length);
|
|
313
|
+
if (__fwd_failure != 0) break;
|
|
314
|
+
if (__fwd_hop(name, length) || nominated) continue;
|
|
315
|
+
if (fastly_http_resp_header_append(__fwd_output, name, length, load<usize>(pair + 8), load<i32>(pair + 12)) != 0) { __fwd_fail(502); break; }
|
|
316
|
+
}
|
|
317
|
+
__pulse_fetch_close_response(__fwd_response); __fwd_response = -1;
|
|
318
|
+
if (!__fwd_remaining()) { __fwd_error_response(); return 0; }
|
|
319
|
+
// Treat even an ambiguous send failure as committed: never emit a second
|
|
320
|
+
// response. The writable body remains ours until explicit EOF or abandon.
|
|
321
|
+
__fwd_committed = true;
|
|
322
|
+
const output = __fwd_output; __fwd_output = -1;
|
|
323
|
+
if (fastly_http_resp_send_downstream(output, __fwd_downstream, 1) != 0) { __pulse_fetch_close_response(output); __fwd_fail(502); __fwd_cleanup(); return 0; }
|
|
324
|
+
const bodyless = __fwd_status == 204 || __fwd_status == 205 || __fwd_status == 304;
|
|
325
|
+
const eof = __fwd_copy(__fwd_body, __fwd_downstream, bodyless ? 0 : __FWD_RESPONSE_MAX, bodyless ? 0 : __fwd_response_length, false);
|
|
326
|
+
__pulse_fetch_close_body(__fwd_body); __fwd_body = -1;
|
|
327
|
+
if (eof && __fwd_remaining()) {
|
|
328
|
+
const downstream = __fwd_downstream; __fwd_downstream = -1;
|
|
329
|
+
if (__pulse_fetch_close_body(downstream) != 0) __fwd_fail(502);
|
|
330
|
+
// Success is the host-handoff boundary. The handle is gone; there is no
|
|
331
|
+
// final-writer wait, delivery receipt, or post-handoff timeout guarantee.
|
|
332
|
+
}
|
|
333
|
+
__fwd_cleanup(); return 0;
|
|
334
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const {normalizeBodyForwarding, forwardingError} = require('../toolchain/body-forwarding.js');
|
|
5
|
+
const IMPORTS = ['fastly_http_req:send_async_streaming', 'fastly_http_body:abandon',
|
|
6
|
+
'fastly_async_io:is_ready', 'fastly_http_resp:header_names_get'];
|
|
7
|
+
const enabled = plan => (plan?.capabilities || []).includes('request.body.forward');
|
|
8
|
+
function field(value, name) { return value?.entries?.find(entry => entry.key?.value === name)?.value; }
|
|
9
|
+
function validate(plan, options) {
|
|
10
|
+
const config = normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs);
|
|
11
|
+
if (!enabled(plan)) return;
|
|
12
|
+
if (!config) throw forwardingError('Fastly Native incoming forwarding requires fastly.bodyForwarding limits and maxDurationMs.');
|
|
13
|
+
const effects = plan.effects || [], effect = effects[0];
|
|
14
|
+
const init = effect?.inputs?.find(input => input.name === 'init')?.value;
|
|
15
|
+
const body = field(init, 'body'), method = field(init, 'method'), headers = field(init, 'headers');
|
|
16
|
+
if (effects.length !== 1 || effect.kind !== 'fetch' || effect.grouped
|
|
17
|
+
|| effect.result?.mode !== 'return' || effect.result?.valueKind !== 'opaque-response'
|
|
18
|
+
|| effect.resource?.kind !== 'literal' || options.bindings?.dynamicBackends
|
|
19
|
+
|| body?.name !== 'request.body.forward-marker' || method?.kind !== 'literal' || method.value !== 'POST'
|
|
20
|
+
|| init.entries.some(entry => !['method', 'body', 'headers', 'timeoutMs'].includes(entry.key?.value))
|
|
21
|
+
|| (headers && (headers.kind !== 'object' || headers.entries.some(entry => entry.key?.kind !== 'literal' || entry.value?.kind !== 'literal' || typeof entry.value.value !== 'string')))) {
|
|
22
|
+
throw forwardingError('Fastly Native forwarding admits one terminal opaque POST fetch with inline req.body(), a static backend, and optional literal headers/timeoutMs; groups, decoding, reuse, and dynamic backends are unsupported.');
|
|
23
|
+
}
|
|
24
|
+
const entries = headers?.entries || [];
|
|
25
|
+
const hop = /^(connection|keep-alive|proxy-connection|transfer-encoding|te|trailer|upgrade|content-length|host|proxy-authenticate|proxy-authorization)$/i;
|
|
26
|
+
if (entries.length > 64 || entries.reduce((sum, entry) => sum + Buffer.byteLength(entry.key.value) + Buffer.byteLength(entry.value.value) + 4, 0) > 16384
|
|
27
|
+
|| entries.some(entry => hop.test(entry.key.value) || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(entry.key.value) || /[\x00-\x08\x0a-\x1f\x7f]/.test(entry.value.value))) {
|
|
28
|
+
throw forwardingError('Forwarding request headers must fit 64 pairs/16 KiB and exclude hop-by-hop, framing and Host fields.');
|
|
29
|
+
}
|
|
30
|
+
// One marker evaluation owns the incoming body. Reject unused/duplicate markers
|
|
31
|
+
// too: they must not be dispatched later through another continuation.
|
|
32
|
+
let markers = 0;
|
|
33
|
+
function walk(value) { if (!value || typeof value !== 'object') return; if (value.name === 'request.body.forward-marker') markers++; for (const item of Object.values(value)) walk(item); }
|
|
34
|
+
walk(plan);
|
|
35
|
+
if (markers !== 1) throw forwardingError('Fastly Native forwarding requires exactly one incoming body consumer.');
|
|
36
|
+
}
|
|
37
|
+
function instrument(input, plan, bindings) {
|
|
38
|
+
if (!enabled(plan)) return input;
|
|
39
|
+
let source = input;
|
|
40
|
+
const replace = (before, after) => {
|
|
41
|
+
if (!source.includes(before)) throw new Error('Fastly body-forwarding boundary drift: ' + before.slice(0, 100));
|
|
42
|
+
source = source.replace(before, after);
|
|
43
|
+
};
|
|
44
|
+
for (const [name, args, result, body] of [
|
|
45
|
+
['__pulse_fastly_fetch_begin', 'effectIndex: i32, payload: __PulseFastlyValue', 'void', '__fwd_begin(effectIndex, payload);'],
|
|
46
|
+
['__pulse_fastly_wait_fetch', 'effectIndex: i32', 'i32', 'return __fwd_wait();']
|
|
47
|
+
]) {
|
|
48
|
+
const line = source.split('\n').find(line => line.startsWith(`function ${name}(`));
|
|
49
|
+
if (!line || !line.endsWith('}')) throw new Error('Fastly forwarding function boundary drift: ' + name);
|
|
50
|
+
replace(line, `function ${name}(${args}): ${result} { ${body} }`);
|
|
51
|
+
}
|
|
52
|
+
replace('function __pulse_fastly_send_result(handle: i32): i32 {',
|
|
53
|
+
'function __pulse_fastly_send_result(handle: i32): i32 { if (handle == __fwd_result && handle != 0) return __fwd_send();');
|
|
54
|
+
replace('function __pulse_fastly_jwt_send_error(): void {',
|
|
55
|
+
'function __pulse_fastly_jwt_send_error(): void { if (__fwd_error_response()) return;');
|
|
56
|
+
replace('function __request_send_timeout(): void {',
|
|
57
|
+
'function __request_send_timeout(): void { if (__fwd_committed) { __fwd_cleanup(); return; }');
|
|
58
|
+
replace(' __pulse_fastly_run_invocation()\n', ' __pulse_fastly_run_invocation()\n __fwd_cleanup()\n');
|
|
59
|
+
replace(' __pulse_fastly_request_body_handle = load<i32>(changetype<usize>(handles) + 4)',
|
|
60
|
+
' __pulse_fastly_request_body_handle = load<i32>(changetype<usize>(handles) + 4)\n __fwd_source = __pulse_fastly_request_body_handle');
|
|
61
|
+
// Generic request-budget code must not wait for headers before driving upload.
|
|
62
|
+
const begin = source.indexOf('function __pulse_fastly_resolve_effect(');
|
|
63
|
+
const end = source.indexOf('\n}', begin);
|
|
64
|
+
if (begin < 0 || end < 0) throw new Error('Fastly forwarding effect boundary drift');
|
|
65
|
+
source = source.slice(0, begin) + 'function __pulse_fastly_resolve_effect(effectIndex: i32): i32 { return __fwd_wait(); }' + source.slice(end + 2);
|
|
66
|
+
return source + '\n' + `const __FWD_REQUEST_MAX: i32 = ${bindings.bodyForwarding.requestMaxBytes}\nconst __FWD_RESPONSE_MAX: i32 = ${bindings.bodyForwarding.responseMaxBytes}\n`
|
|
67
|
+
+ fs.readFileSync(path.join(__dirname, 'native-body-forwarding.as.ts'), 'utf8');
|
|
68
|
+
}
|
|
69
|
+
module.exports = {IMPORTS, enabled, validate, instrument};
|
|
@@ -0,0 +1,161 @@
|
|
|
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>(4), 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
|
+
const status = __pulse_fetch_poll(__pulse_fetch_retired[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
|
|
81
|
+
if (status == 0 && done[0] == 0) continue
|
|
82
|
+
if (status == 0 && done[0] == 1) { __pulse_fetch_close_response(response[0]); __pulse_fetch_close_body(body[0]) }
|
|
83
|
+
__pulse_fetch_retired[i] = __pulse_fetch_retired[__pulse_fetch_retired.length - 1]; __pulse_fetch_retired.pop()
|
|
84
|
+
}
|
|
85
|
+
// Unready handles remain platform-owned until host teardown. No guest task
|
|
86
|
+
// survives _start and no response/body is exposed for an expired operation.
|
|
87
|
+
}
|
|
88
|
+
// Request-budget instrumentation replaces these hooks when maxDurationMs exists.
|
|
89
|
+
function __pulse_fetch_request_check(index: i32): bool { return true }
|
|
90
|
+
function __pulse_fetch_request_remaining(): i32 { return 2147483647 }
|
|
91
|
+
function __pulse_fetch_take(index: i32): i32 {
|
|
92
|
+
const outcome = __pulse_fetch_outcome[index], result = __pulse_fastly_ready[index]
|
|
93
|
+
__pulse_fetch_completed[index] = false; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_NONE
|
|
94
|
+
__pulse_fastly_ready[index] = 0
|
|
95
|
+
if (outcome == -1) __pulse_fastly_fail(PULSE_ERROR_TRANSPORT, 271, index)
|
|
96
|
+
else if (outcome == -2) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index)
|
|
97
|
+
else if (outcome != 0) __pulse_fetch_fail(outcome, 81, index)
|
|
98
|
+
return outcome == 0 ? result : 0
|
|
99
|
+
}
|
|
100
|
+
function __pulse_fetch_wait(index: i32): i32 {
|
|
101
|
+
const handles = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT), selected = new StaticArray<i32>(1)
|
|
102
|
+
const detail = new StaticArray<i32>(4), done = new StaticArray<i32>(1)
|
|
103
|
+
const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1)
|
|
104
|
+
while (!__pulse_fetch_completed[index]) {
|
|
105
|
+
if (!__pulse_fetch_request_check(index)) { __pulse_fetch_retire_all(); return 0 }
|
|
106
|
+
let now = __pulse_fetch_clock(index)
|
|
107
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
108
|
+
let count = 0, remaining = 2147483647.0
|
|
109
|
+
// Observe all dispatched fetches, not just the first static slot. Otherwise
|
|
110
|
+
// a slow earlier sibling could turn timely later headers into a false timeout.
|
|
111
|
+
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
|
|
112
|
+
if (!__pulse_fetch_active(i)) continue
|
|
113
|
+
if (__pulse_fetch_remaining(i, now) <= 0) { __pulse_fetch_retire(i, -1); continue }
|
|
114
|
+
done[0] = 0
|
|
115
|
+
const status = __pulse_fetch_poll(__pulse_fastly_pending[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
|
|
116
|
+
now = __pulse_fetch_clock(i)
|
|
117
|
+
if (status != 0) {
|
|
118
|
+
__pulse_fetch_complete(i, now < 0 ? -2 : __pulse_fetch_remaining(i, now) <= 0 ? -1 : status)
|
|
119
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (done[0] != 0 && done[0] != 1) {
|
|
123
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, i); __pulse_fetch_retire_all(); return 0
|
|
124
|
+
}
|
|
125
|
+
if (now < 0 || __pulse_fetch_remaining(i, now) <= 0) {
|
|
126
|
+
if (done[0] == 1) {
|
|
127
|
+
__pulse_fetch_close_response(response[0]); __pulse_fetch_close_body(body[0]); __pulse_fetch_complete(i, now < 0 ? -2 : -1)
|
|
128
|
+
} else __pulse_fetch_retire(i, now < 0 ? -2 : -1)
|
|
129
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (done[0] == 1) {
|
|
133
|
+
const value = new __PulseFastlyValue(); value.kind = PULSE_VALUE_FETCH
|
|
134
|
+
value.responseHandle = response[0]; value.bodyHandle = body[0]
|
|
135
|
+
__pulse_fetch_complete(i, 0, __pulse_fastly_put(value)); continue
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (__pulse_fetch_completed[index]) break
|
|
139
|
+
if (!__pulse_fetch_request_check(index)) { __pulse_fetch_retire_all(); return 0 }
|
|
140
|
+
now = __pulse_fetch_clock(index)
|
|
141
|
+
if (now < 0) { __pulse_fetch_retire_all(); return 0 }
|
|
142
|
+
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
|
|
143
|
+
if (!__pulse_fetch_active(i)) continue
|
|
144
|
+
const budget = __pulse_fetch_remaining(i, now)
|
|
145
|
+
if (budget <= 0) { __pulse_fetch_retire(i, -1); continue }
|
|
146
|
+
handles[count++] = __pulse_fastly_pending[i]; remaining = Math.min(remaining, budget)
|
|
147
|
+
}
|
|
148
|
+
if (__pulse_fetch_completed[index]) break
|
|
149
|
+
remaining = Math.min(remaining, f64(__pulse_fetch_request_remaining()))
|
|
150
|
+
if (count == 0 || remaining <= 0) {
|
|
151
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); __pulse_fetch_retire_all(); return 0
|
|
152
|
+
}
|
|
153
|
+
selected[0] = -2
|
|
154
|
+
const status = __pulse_fetch_select(changetype<usize>(handles), count, i32(Math.ceil(remaining)), changetype<usize>(selected))
|
|
155
|
+
const after = __pulse_fetch_clock(index)
|
|
156
|
+
if (status != 0 || selected[0] < -1 || selected[0] >= count || after < 0 || (selected[0] == -1 && after <= now)) {
|
|
157
|
+
__pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); __pulse_fetch_retire_all(); return 0
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return __pulse_fetch_take(index)
|
|
161
|
+
}
|
|
@@ -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,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function runtimeSource() {
|
|
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
|
+
__pulse_fastly_fail(network ? PULSE_ERROR_TRANSPORT : PULSE_ERROR_HOSTCALL, stage, effect);
|
|
14
|
+
}
|
|
15
|
+
function __pulse_fetch_failed(): bool {
|
|
16
|
+
return (__pulse_fastly_last_error == PULSE_ERROR_TRANSPORT || __pulse_fastly_last_error == PULSE_ERROR_HOSTCALL || __pulse_fastly_last_error == PULSE_ERROR_VALUE)
|
|
17
|
+
&& (__pulse_fastly_error_stage == 77 || __pulse_fastly_error_stage == 81 || (__pulse_fastly_error_stage >= 270 && __pulse_fastly_error_stage <= 272))
|
|
18
|
+
&& __pulse_fastly_effect_kind(__pulse_fastly_error_effect) == 1;
|
|
19
|
+
}
|
|
20
|
+
function __pulse_fetch_send_error(): bool {
|
|
21
|
+
if (!__pulse_fetch_failed()) return false;
|
|
22
|
+
const output = new __PulseFastlyValue(); output.kind = PULSE_VALUE_RESPONSE;
|
|
23
|
+
const network = __pulse_fastly_last_error == PULSE_ERROR_TRANSPORT;
|
|
24
|
+
output.status = __pulse_fastly_error_stage == 270 ? 400 : __pulse_fastly_error_stage == 271 ? 504 : network ? 502 : 500;
|
|
25
|
+
output.text = __pulse_fastly_error_stage == 270 ? "Invalid fetch timeoutMs" : __pulse_fastly_error_stage == 271 ? "Gateway Timeout" : network ? "Bad Gateway" : "Internal Server Error";
|
|
26
|
+
output.headers.push(new __PulseFastlyHeader("content-type", "text/plain; charset=utf-8"));
|
|
27
|
+
__pulse_fastly_send_result(__pulse_fastly_put(output));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
function instrument(source) {
|
|
33
|
+
// Keep the fatal marker for the deadline owner, which runs after this pass.
|
|
34
|
+
source = source.replace('if (fatal) return',
|
|
35
|
+
'if (fatal && __pulse_fetch_failed()) { __pulse_fastly_jwt_send_error(); return; }\n if (fatal) return');
|
|
36
|
+
const marker = 'function __pulse_fastly_jwt_send_error(): void {';
|
|
37
|
+
if (!source.includes(marker)) throw new Error('Fastly fetch error boundary drift.');
|
|
38
|
+
return source.replace(marker, marker + ' if (__pulse_fetch_send_error()) return;');
|
|
39
|
+
}
|
|
40
|
+
module.exports = {runtimeSource, instrument};
|