@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.
Files changed (68) hide show
  1. package/README.md +7 -6
  2. package/dist/config-schema.json +30 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +11 -10
  8. package/src/build/canonical-target.js +35 -2
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +49 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/entities-native.js +6 -11
  13. package/src/build/kv-native.as.ts +331 -0
  14. package/src/build/kv-native.js +27 -0
  15. package/src/build/native-application-errors.js +159 -0
  16. package/src/build/native-binary-resources.js +30 -0
  17. package/src/build/native-body-forwarding.as.ts +341 -0
  18. package/src/build/native-body-forwarding.js +69 -0
  19. package/src/build/native-dynamic-backends.as.ts +115 -0
  20. package/src/build/native-dynamic-backends.js +52 -0
  21. package/src/build/native-fetch-deadlines.as.ts +164 -0
  22. package/src/build/native-fetch-deadlines.js +27 -0
  23. package/src/build/native-fetch-errors.js +38 -0
  24. package/src/build/native-handle-ownership.js +127 -0
  25. package/src/build/native-header-lookup.js +50 -0
  26. package/src/build/native-http-effects.js +155 -32
  27. package/src/build/native-platform-capabilities.js +755 -210
  28. package/src/build/native-request-body.js +44 -0
  29. package/src/build/native-request-headers.js +93 -0
  30. package/src/build/native-send-error-detail.as.ts +12 -0
  31. package/src/build/native-stdio.js +45 -0
  32. package/src/build/native-string-values.js +143 -0
  33. package/src/build/native-transport-details.as.ts +84 -0
  34. package/src/build/native-value-budget.js +90 -0
  35. package/src/build/package-native-application.js +114 -0
  36. package/src/build/request-budget.as.ts +50 -0
  37. package/src/build/request-budget.js +47 -0
  38. package/src/build/s3-body.as.ts +97 -0
  39. package/src/build/s3-native.as.ts +228 -0
  40. package/src/build/s3-native.js +30 -0
  41. package/src/build/schema-admission.js +83 -0
  42. package/src/build/schema-nested-json.js +93 -0
  43. package/src/build/schema-scalar-record.js +41 -0
  44. package/src/build/time-native.js +49 -0
  45. package/src/build/wasm-budget.js +28 -0
  46. package/src/config-api.js +6 -0
  47. package/src/config-schema.json +30 -0
  48. package/src/index.ts +25 -0
  49. package/src/javascript/jwt-verifier.js +14 -4
  50. package/src/javascript/logging.js +11 -2
  51. package/src/javascript/package-effects.js +5 -2
  52. package/src/javascript/runtime-host.js +7 -3
  53. package/src/javascript/source-package.js +4 -2
  54. package/src/javascript/target-support-policy.js +20 -3
  55. package/src/javascript/target.js +17 -1
  56. package/src/javascript/test-runtime.js +1 -0
  57. package/src/provider-contract.js +22 -6
  58. package/src/runtime/canonical-api-runtime.js +6 -0
  59. package/src/testing/conditional-kv-host.js +155 -0
  60. package/src/testing/native-dynamic-backends-host.js +39 -0
  61. package/src/testing/native-http-effects-host.js +9 -1
  62. package/src/testing/native-platform-capabilities-host.js +349 -24
  63. package/src/toolchain/body-forwarding.js +25 -0
  64. package/src/toolchain/config.js +16 -0
  65. package/src/toolchain/index.js +48 -5
  66. package/src/toolchain/logging.js +18 -0
  67. package/src/toolchain/native-execution.js +78 -0
  68. 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};