@pulse-compute/provider-fastly 1.0.0-beta.1 → 1.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) 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 +41 -2
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +51 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/entities-native.js +6 -11
  13. package/src/build/host-extensions.js +254 -0
  14. package/src/build/kv-native.as.ts +331 -0
  15. package/src/build/kv-native.js +27 -0
  16. package/src/build/lifecycle.js +50 -0
  17. package/src/build/native-application-errors.js +159 -0
  18. package/src/build/native-binary-resources.js +30 -0
  19. package/src/build/native-body-forwarding.as.ts +325 -0
  20. package/src/build/native-body-forwarding.js +73 -0
  21. package/src/build/native-dynamic-backends.as.ts +115 -0
  22. package/src/build/native-dynamic-backends.js +52 -0
  23. package/src/build/native-fetch-accounting.as.ts +133 -0
  24. package/src/build/native-fetch-accounting.js +84 -0
  25. package/src/build/native-fetch-bounds.js +47 -0
  26. package/src/build/native-fetch-cohorts.as.ts +126 -0
  27. package/src/build/native-fetch-cohorts.js +65 -0
  28. package/src/build/native-fetch-deadlines.as.ts +169 -0
  29. package/src/build/native-fetch-deadlines.js +36 -0
  30. package/src/build/native-fetch-errors.js +38 -0
  31. package/src/build/native-fetch-headers.js +52 -0
  32. package/src/build/native-fetch-outcomes.js +87 -0
  33. package/src/build/native-fetch-timing.as.ts +74 -0
  34. package/src/build/native-fetch-timing.js +50 -0
  35. package/src/build/native-forwarding-accounting.as.ts +153 -0
  36. package/src/build/native-forwarding-accounting.js +47 -0
  37. package/src/build/native-handle-ownership.js +127 -0
  38. package/src/build/native-header-lookup.js +50 -0
  39. package/src/build/native-http-effects.js +164 -35
  40. package/src/build/native-platform-capabilities.js +815 -220
  41. package/src/build/native-request-body.js +45 -0
  42. package/src/build/native-request-headers.js +93 -0
  43. package/src/build/native-send-error-detail.as.ts +12 -0
  44. package/src/build/native-stdio.js +45 -0
  45. package/src/build/native-string-values.js +143 -0
  46. package/src/build/native-transport-details.as.ts +84 -0
  47. package/src/build/native-value-budget.js +90 -0
  48. package/src/build/package-native-application.js +114 -0
  49. package/src/build/request-budget-source.js +58 -0
  50. package/src/build/request-budget.js +46 -0
  51. package/src/build/response-authority.js +56 -0
  52. package/src/build/s3-body.as.ts +97 -0
  53. package/src/build/s3-native.as.ts +228 -0
  54. package/src/build/s3-native.js +30 -0
  55. package/src/build/schema-admission.js +83 -0
  56. package/src/build/schema-nested-json.js +93 -0
  57. package/src/build/schema-scalar-record.js +41 -0
  58. package/src/build/time-native.js +49 -0
  59. package/src/build/wasm-budget.js +28 -0
  60. package/src/config-api.js +6 -0
  61. package/src/config-schema.json +30 -0
  62. package/src/index.ts +25 -0
  63. package/src/javascript/fetch-adapter.js +3 -0
  64. package/src/javascript/jwt-verifier.js +14 -4
  65. package/src/javascript/logging.js +11 -2
  66. package/src/javascript/package-effects.js +5 -2
  67. package/src/javascript/runtime-host.js +8 -3
  68. package/src/javascript/source-package.js +4 -2
  69. package/src/javascript/target-support-policy.js +20 -3
  70. package/src/javascript/target.js +17 -1
  71. package/src/javascript/test-runtime.js +1 -0
  72. package/src/provider-contract.js +23 -6
  73. package/src/runtime/canonical-api-runtime.js +6 -0
  74. package/src/testing/conditional-kv-host.js +155 -0
  75. package/src/testing/native-dynamic-backends-host.js +39 -0
  76. package/src/testing/native-http-effects-host.js +18 -1
  77. package/src/testing/native-platform-capabilities-host.js +359 -24
  78. package/src/toolchain/body-forwarding.js +25 -0
  79. package/src/toolchain/config.js +16 -0
  80. package/src/toolchain/index.js +49 -5
  81. package/src/toolchain/logging.js +18 -0
  82. package/src/toolchain/native-execution.js +83 -0
  83. package/src/toolchain/s3.js +26 -0
@@ -0,0 +1,169 @@
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 /*ACCOUNTING_UNREAPED*/ }
19
+ /*ACCOUNTING_CLOCK_STORAGE*/
20
+ function __pulse_fetch_clock(index: i32): i64 {
21
+ const out = /*ACCOUNTING_CLOCK_OUT*/
22
+ if (wasi_snapshot_preview1_clock_time_get(1, 1, changetype<usize>(out)) != 0
23
+ || out[0] < 0 || out[0] < __pulse_fetch_last_clock) {
24
+ /*ACCOUNTING_CLOCK_FAILURE*/
25
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); return -1
26
+ }
27
+ __pulse_fetch_last_clock = out[0]; return out[0]
28
+ }
29
+ function __pulse_fetch_begin_deadline(index: i32, init: __PulseFastlyValue): void {
30
+ __pulse_fetch_timeout[index] = 0; __pulse_fetch_completed[index] = false; __pulse_fetch_outcome[index] = 0
31
+ const field = __pulse_fastly_find(init, 'timeoutMs')
32
+ if (field < 0) return
33
+ const value = __pulse_fastly_value(unchecked(init.values[field]))
34
+ if (value.kind == PULSE_VALUE_UNDEFINED) return
35
+ const ms = value.number
36
+ if (value.kind != PULSE_VALUE_NUMBER || ms <= 0 || ms > 9007199254740991 || Math.floor(ms) != ms) {
37
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 270, index); return
38
+ }
39
+ __pulse_fetch_timeout[index] = ms
40
+ __pulse_fetch_started[index] = __pulse_fetch_clock(index)
41
+ }
42
+ function __pulse_fetch_remaining(index: i32, now: i64): f64 {
43
+ if (__pulse_fetch_timeout[index] == 0) return 2147483647.0
44
+ // Subtract elapsed nanoseconds before converting. Do not overflow by adding
45
+ // a safe-integer millisecond option to an i64 nanosecond clock.
46
+ return __pulse_fetch_timeout[index] - f64(now - __pulse_fetch_started[index]) / 1000000.0
47
+ }
48
+ function __pulse_fetch_before_send(index: i32): bool {
49
+ if (__pulse_fastly_effect_kind(index) != 1 || __pulse_fetch_timeout[index] == 0) return true
50
+ const now = __pulse_fetch_clock(index)
51
+ if (now < 0) return false
52
+ if (__pulse_fetch_remaining(index, now) > 0) return true
53
+ __pulse_fastly_fail(PULSE_ERROR_TRANSPORT, 271, index); return false
54
+ }
55
+ function __pulse_fetch_active(index: i32): bool {
56
+ return __pulse_fastly_effect_kind(index) == 1 && __pulse_fastly_pending_mode[index] == PULSE_FASTLY_PENDING_ASYNC
57
+ }
58
+ function __pulse_fetch_has_deadlines(): bool {
59
+ for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
60
+ /*ACCOUNTING_FORCE_POLL*/
61
+ if (__pulse_fetch_active(i) && __pulse_fetch_timeout[i] > 0) return true
62
+ }
63
+ return false
64
+ }
65
+ function __pulse_fetch_complete(index: i32, outcome: i32, result: i32 = 0): void {
66
+ __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_READY
67
+ __pulse_fastly_pending[index] = 0; __pulse_fastly_ready[index] = result
68
+ __pulse_fetch_completed[index] = true; __pulse_fetch_outcome[index] = outcome
69
+ }
70
+ function __pulse_fetch_retire(index: i32, outcome: i32): void {
71
+ // The Fastly ABI has no pending-request cancellation hostcall. Retain explicit
72
+ // ownership until invocation cleanup; never wait, retry, or claim rollback.
73
+ /*ACCOUNTING_RETIRED*/ __pulse_fetch_retired_total++
74
+ __pulse_fetch_complete(index, outcome)
75
+ }
76
+ function __pulse_fetch_retire_all(): void {
77
+ for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) if (__pulse_fetch_active(i)) __pulse_fetch_retire(i, -2)
78
+ }
79
+ function __pulse_fetch_reap(): void {
80
+ /*ACCOUNTING_REAP*/
81
+ const detail = new StaticArray<i32>(__PULSE_SEND_ERROR_DETAIL_WORDS), done = new StaticArray<i32>(1)
82
+ const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1)
83
+ for (let i = __pulse_fetch_retired.length - 1; i >= 0; i--) {
84
+ done[0] = 0
85
+ __pulse_send_error_detail_reset(changetype<usize>(detail))
86
+ const status = /*ACCOUNTING_POLL*/(__pulse_fetch_retired[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
87
+ if (status == 0 && done[0] == 0) continue
88
+ if (status == 0 && done[0] == 1) { /*ACCOUNTING_LATE_CLOSE*/ }
89
+ __pulse_fetch_retired[i] = __pulse_fetch_retired[__pulse_fetch_retired.length - 1]; __pulse_fetch_retired.pop()
90
+ }
91
+ // Unready handles remain platform-owned until host teardown. No guest task
92
+ // survives _start and no response/body is exposed for an expired operation.
93
+ }
94
+ // Request-budget instrumentation replaces these hooks when maxDurationMs exists.
95
+ function __pulse_fetch_request_check(index: i32): bool { return true }
96
+ function __pulse_fetch_request_remaining(): i32 { return 2147483647 }
97
+ function __pulse_fetch_take(index: i32): i32 {
98
+ const outcome = __pulse_fetch_outcome[index], result = __pulse_fastly_ready[index]
99
+ __pulse_fetch_completed[index] = false; __pulse_fastly_pending_mode[index] = PULSE_FASTLY_PENDING_NONE
100
+ __pulse_fastly_ready[index] = 0
101
+ if (outcome == -1) __pulse_fastly_fail(PULSE_ERROR_TRANSPORT, 271, index)
102
+ else if (outcome == -2) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index)
103
+ else if (outcome != 0) __pulse_fetch_fail(outcome, 81, index)
104
+ return outcome == 0 ? result : 0
105
+ }
106
+ function __pulse_fetch_wait(index: i32): i32 {
107
+ const handles = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT), selected = new StaticArray<i32>(1)
108
+ const detail = new StaticArray<i32>(__PULSE_SEND_ERROR_DETAIL_WORDS), done = new StaticArray<i32>(1)
109
+ const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1)
110
+ while (!__pulse_fetch_completed[index]) {
111
+ if (!__pulse_fetch_request_check(index)) { __pulse_fetch_retire_all(); return 0 }
112
+ let now = __pulse_fetch_clock(index)
113
+ if (now < 0) { __pulse_fetch_retire_all(); return 0 }
114
+ let count = 0, remaining = 2147483647.0
115
+ // Observe all dispatched fetches, not just the first static slot. Otherwise
116
+ // a slow earlier sibling could turn timely later headers into a false timeout.
117
+ for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
118
+ if (!__pulse_fetch_active(i)) continue
119
+ if (__pulse_fetch_remaining(i, now) <= 0) { __pulse_fetch_retire(i, -1); continue }
120
+ done[0] = 0
121
+ __pulse_send_error_detail_reset(changetype<usize>(detail))
122
+ const status = /*ACCOUNTING_POLL*/(__pulse_fastly_pending[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
123
+ now = __pulse_fetch_clock(i)
124
+ if (status != 0) {
125
+ __pulse_transport_capture(i, status, 'poll', changetype<usize>(detail))
126
+ __pulse_fetch_complete(i, now < 0 ? -2 : __pulse_fetch_remaining(i, now) <= 0 ? -1 : status)
127
+ if (now < 0) { __pulse_fetch_retire_all(); return 0 }
128
+ continue
129
+ }
130
+ if (done[0] != 0 && done[0] != 1) {
131
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, i); __pulse_fetch_retire_all(); return 0
132
+ }
133
+ if (now < 0 || __pulse_fetch_remaining(i, now) <= 0) {
134
+ if (done[0] == 1) {
135
+ /*ACCOUNTING_LATE_CLOSE*/; __pulse_fetch_complete(i, now < 0 ? -2 : -1)
136
+ } else __pulse_fetch_retire(i, now < 0 ? -2 : -1)
137
+ if (now < 0) { __pulse_fetch_retire_all(); return 0 }
138
+ continue
139
+ }
140
+ if (done[0] == 1) {
141
+ const value = new __PulseFastlyValue(); value.kind = PULSE_VALUE_FETCH
142
+ value.responseHandle = response[0]; value.bodyHandle = body[0]
143
+ __pulse_fetch_complete(i, 0, __pulse_fastly_put(value)); continue
144
+ }
145
+ }
146
+ if (__pulse_fetch_completed[index]) break
147
+ if (!__pulse_fetch_request_check(index)) { __pulse_fetch_retire_all(); return 0 }
148
+ now = __pulse_fetch_clock(index)
149
+ if (now < 0) { __pulse_fetch_retire_all(); return 0 }
150
+ for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
151
+ if (!__pulse_fetch_active(i)) continue
152
+ const budget = __pulse_fetch_remaining(i, now)
153
+ if (budget <= 0) { __pulse_fetch_retire(i, -1); continue }
154
+ handles[count++] = __pulse_fastly_pending[i]; remaining = Math.min(remaining, budget)
155
+ }
156
+ if (__pulse_fetch_completed[index]) break
157
+ remaining = Math.min(remaining, f64(__pulse_fetch_request_remaining()))
158
+ if (count == 0 || remaining <= 0) {
159
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); __pulse_fetch_retire_all(); return 0
160
+ }
161
+ selected[0] = -2
162
+ const status = __pulse_fetch_select(changetype<usize>(handles), count, i32(Math.ceil(remaining)), changetype<usize>(selected))
163
+ const after = __pulse_fetch_clock(index)
164
+ if (status != 0 || selected[0] < -1 || selected[0] >= count || after < 0 || (selected[0] == -1 && after <= now)) {
165
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); __pulse_fetch_retire_all(); return 0
166
+ }
167
+ }
168
+ return __pulse_fetch_take(index)
169
+ }
@@ -0,0 +1,36 @@
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, accounting) {
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
+ .replace('/*ACCOUNTING_CLOCK_STORAGE*/', accounting ? 'const __pulse_fetch_clock_out = new StaticArray<i64>(1)' : '')
11
+ .replace('/*ACCOUNTING_CLOCK_OUT*/', accounting ? '__pulse_fetch_clock_out' : 'new StaticArray<i64>(1)')
12
+ .replace('/*ACCOUNTING_CLOCK_FAILURE*/', accounting ? '__fca_clock_failed = true;' : '')
13
+ .replace('/*ACCOUNTING_REAP*/', accounting ? '__fca_cleanup(); return;' : '')
14
+ .replace('/*ACCOUNTING_RETIRED*/', accounting ? '' : '__pulse_fetch_retired.push(__pulse_fastly_pending[index]);')
15
+ .replace('/*ACCOUNTING_UNREAPED*/', accounting?.selection === 'canonical-fetch-cohorts' ? '__fca_unreaped()' : accounting ? '__fetch_ra !== null && __fetch_ra!.bank[2*8+5] != PRL_FREE && __fetch_ra!.bank[2*8+5] != PRL_RESERVED ? 1 : 0' : '__pulse_fetch_retired.length')
16
+ .replace('/*ACCOUNTING_FORCE_POLL*/', accounting?.selection === 'canonical-fetch-cohorts' ? 'if (__pulse_fetch_active(i)) return true;' : '')
17
+ .replaceAll('/*ACCOUNTING_POLL*/', accounting ? '__fca_poll' : '__pulse_fetch_poll')
18
+ .replaceAll('/*ACCOUNTING_LATE_CLOSE*/', accounting ? '__fca_dispose(3); __fca_dispose(4)' : '__pulse_fetch_close_response(response[0]); __pulse_fetch_close_body(body[0])');
19
+ }
20
+ function instrument(input, plan, accounting) {
21
+ if (!enabled(plan)) return input;
22
+ let source = input;
23
+ const replace = (before,after) => {
24
+ if (!source.includes(before)) throw new Error('Fastly fetch deadline boundary drift: ' + before.slice(0,80));
25
+ source = source.replace(before,after);
26
+ };
27
+ replace('const init = initIndex >= 0 ? __pulse_fastly_value(unchecked(payload.values[initIndex])) : new __PulseFastlyValue(); let method',
28
+ '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');
29
+ replace('const backendBytes = __pulse_fastly_utf8(backend); const pendingOut',
30
+ `if (!__pulse_fetch_before_send(effectIndex)) { ${accounting ? '__fca_cleanup();' : '__pulse_fetch_close_request(requestHandle); __pulse_fetch_close_body(bodyHandle);'} return; } const backendBytes = __pulse_fastly_utf8(backend); const pendingOut`);
31
+ replace('function __pulse_fastly_wait_fetch(effectIndex: i32): i32 {',
32
+ '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);');
33
+ replace('function __pulse_invocation_close(): void {', 'function __pulse_invocation_close(): void { __pulse_fetch_reap();');
34
+ return source;
35
+ }
36
+ 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, lifecycle) {
34
+ const marker = `function ${lifecycle?.legacyName('__pulse_fastly_jwt_send_error') || '__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,52 @@
1
+ 'use strict';
2
+
3
+ // Preserve ordered pairs (including repeated names) in the existing key/value
4
+ // carrier. Do not use object_set: it would replace duplicate keys.
5
+ function fetchHeadersSource() {
6
+ return `
7
+ function __pulse_fastly_fetch_header_valid(name: string, value: __PulseFastlyValue): bool {
8
+ if (name.length == 0 || value.kind != PULSE_VALUE_STRING) return false
9
+ for (let i = 0; i < name.length; i++) {
10
+ const c = name.charCodeAt(i)
11
+ if ((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122)) continue
12
+ if ("!#$%&'*+-.^_\u0060|~".indexOf(name.charAt(i)) < 0) return false
13
+ }
14
+ for (let i = 0; i < value.text.length; i++) {
15
+ const c = value.text.charCodeAt(i)
16
+ if (c == 0 || c == 10 || c == 13 || c > 255) return false
17
+ }
18
+ return true
19
+ }
20
+ function __pulse_fastly_fetch_headers(input: __PulseFastlyValue, effectIndex: i32): __PulseFastlyValue {
21
+ const output = new __PulseFastlyValue()
22
+ output.kind = PULSE_VALUE_OBJECT
23
+ if (input.kind == PULSE_VALUE_UNDEFINED) return output
24
+ if (input.kind == PULSE_VALUE_OBJECT) {
25
+ for (let i = 0; i < input.keys.length; i++) {
26
+ if (!__pulse_fastly_fetch_header_valid(input.keys[i], __pulse_fastly_value(input.values[i]))) {
27
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 74, effectIndex); return output
28
+ }
29
+ }
30
+ return input
31
+ }
32
+ if (input.kind == PULSE_VALUE_ARRAY) {
33
+ for (let i = 0; i < input.values.length; i++) {
34
+ const pair = __pulse_fastly_value(input.values[i])
35
+ if (pair.kind != PULSE_VALUE_ARRAY || pair.values.length != 2) {
36
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 74, effectIndex); return output
37
+ }
38
+ const name = __pulse_fastly_value(pair.values[0])
39
+ if (name.kind != PULSE_VALUE_STRING || !__pulse_fastly_fetch_header_valid(name.text, __pulse_fastly_value(pair.values[1]))) {
40
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 74, effectIndex); return output
41
+ }
42
+ output.keys.push(name.text); output.values.push(pair.values[1])
43
+ }
44
+ return output
45
+ }
46
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 74, effectIndex)
47
+ return output
48
+ }
49
+ `;
50
+ }
51
+
52
+ module.exports = { fetchHeadersSource };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ function instrument(input, plan) {
4
+ const selected = plan.effects.flatMap((effect, index) => effect.kind === 'fetch' && effect.fetchErrors === 'collect' ? [index] : []);
5
+ const wrapped = plan.effects.flatMap((effect,index)=>effect.kind==='fetch' && (effect.fetchErrors==='collect'||effect.fetchTiming) ? [index] : []);
6
+ const hasTiming = plan.effects.some(effect=>effect.fetchTiming);
7
+ let source = input;
8
+ const replace = (before, after) => {
9
+ if (source.split(before).length !== 2) throw Error('Fetch outcome boundary drift: ' + before);
10
+ source = source.replace(before, after);
11
+ };
12
+ // Even an unselected operation may receive a dynamic init object. Fail closed
13
+ // if it tries to change the statically planned result shape before dispatch.
14
+ const selectedIndex = selected.map(i => `effectIndex == ${i}`).join(' || ') || 'false';
15
+ const anchor = 'headers.kind = PULSE_VALUE_OBJECT; if (init.kind == PULSE_VALUE_OBJECT) {';
16
+ replace(anchor, anchor + `
17
+ const errorsIndex = __pulse_fastly_find(init, 'errors');
18
+ const errors = errorsIndex >= 0 ? __pulse_fastly_value(init.values[errorsIndex]) : new __PulseFastlyValue();
19
+ const collect = errors.kind == PULSE_VALUE_STRING && errors.text == 'collect';
20
+ if ((errorsIndex >= 0 && errors.kind != PULSE_VALUE_UNDEFINED && !collect) || collect != (${selectedIndex})) {
21
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 48, effectIndex); return;
22
+ }
23
+ `);
24
+ if (!wrapped.length) return source;
25
+ replace('function __pulse_fastly_fetch_begin(effectIndex: i32, payload: __PulseFastlyValue): void {',
26
+ 'function __pulse_fetch_begin_uncollected(effectIndex: i32, payload: __PulseFastlyValue): void {');
27
+ replace('function __pulse_fastly_resolve_effect(effectIndex: i32): i32 {',
28
+ 'function __pulse_fetch_resolve_uncollected(effectIndex: i32): i32 {');
29
+ // Keep host status available only while an explicitly selected fetch projection
30
+ // runs. Schema, invalid handle/ABI and request-body errors stay fatal.
31
+ replace('if (status != FASTLY_STATUS_OK) { __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 20, effectIndex); return "" }',
32
+ 'if (status != FASTLY_STATUS_OK) { if (__pulse_fetch_projecting >= 0) __pulse_fetch_fail(status, 20, __pulse_fetch_projecting); else __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 20, effectIndex); return "" }');
33
+ source += `
34
+ // Per-site storage is reset on every admission; it never outlives invocation ownership.
35
+ const __pulse_fetch_collected_failure = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
36
+ let __pulse_fetch_projecting: i32 = -1
37
+ function __pulse_fetch_collects(index: i32): bool { return ${selected.map(i => `index == ${i}`).join(' || ') || 'false'} }
38
+ function __pulse_fetch_wraps(index: i32): bool { return ${wrapped.map(i => `index == ${i}`).join(' || ')} }
39
+ function __pulse_fetch_envelope(index: i32, value: i32, timeout: bool = false, rejected: bool = false): i32 {
40
+ const result = host_value_object()
41
+ host_value_object_set(result, __pulse_fastly_string_value('status'), __pulse_fastly_string_value(rejected ? 'rejected' : 'fulfilled'))
42
+ if (rejected) {
43
+ const error = host_value_object()
44
+ host_value_object_set(error, __pulse_fastly_string_value('code'), __pulse_fastly_string_value(timeout ? 'PULSE_FETCH_TIMEOUT' : 'PULSE_FETCH_NETWORK'))
45
+ host_value_object_set(result, __pulse_fastly_string_value('error'), error)
46
+ } else host_value_object_set(result, __pulse_fastly_string_value('value'), value)
47
+ ${hasTiming ? "if (__pulse_timing_selected(index)) host_value_object_set(result, __pulse_fastly_string_value('elapsedMs'), __pulse_timing_elapsed(index))" : ''}
48
+ return result
49
+ }
50
+ function __pulse_fetch_collect_failure(index: i32): i32 {
51
+ if (!__pulse_fetch_collects(index) || !__pulse_fetch_request_check(index)) return 0
52
+ const stage = __pulse_fastly_error_stage
53
+ if (__pulse_fastly_last_error != PULSE_ERROR_TRANSPORT || __pulse_fastly_error_effect != index
54
+ || (stage != 77 && stage != 81 && stage != 271 && stage != 20)) return 0
55
+ __pulse_fastly_last_error = 0; __pulse_fastly_error_stage = 0; __pulse_fastly_error_effect = -1
56
+ return __pulse_fetch_envelope(index, 0, stage == 271, true)
57
+ }
58
+ function __pulse_fastly_fetch_begin(effectIndex: i32, payload: __PulseFastlyValue): void {
59
+ __pulse_fetch_collected_failure[effectIndex] = 0
60
+ __pulse_fetch_begin_uncollected(effectIndex, payload)
61
+ if (__pulse_fastly_last_error == 0) return
62
+ const failure = __pulse_fetch_collect_failure(effectIndex)
63
+ if (failure <= 0) return
64
+ __pulse_fetch_collected_failure[effectIndex] = failure
65
+ __pulse_fastly_ready_effect(effectIndex, failure)
66
+ }
67
+ function __pulse_fastly_resolve_effect(effectIndex: i32): i32 {
68
+ const saved = __pulse_fetch_collected_failure[effectIndex]
69
+ if (saved > 0) {
70
+ if (!__pulse_fetch_request_check(effectIndex)) return 0
71
+ __pulse_fetch_collected_failure[effectIndex] = 0
72
+ __pulse_fastly_pending_mode[effectIndex] = PULSE_FASTLY_PENDING_NONE
73
+ __pulse_fastly_ready[effectIndex] = 0
74
+ return saved
75
+ }
76
+ if (__pulse_fetch_wraps(effectIndex)) __pulse_fetch_projecting = effectIndex
77
+ const value = __pulse_fetch_resolve_uncollected(effectIndex)
78
+ __pulse_fetch_projecting = -1
79
+ if (!__pulse_fetch_wraps(effectIndex)) return value
80
+ if (!__pulse_fetch_request_check(effectIndex)) return 0
81
+ if (__pulse_fastly_last_error != 0) return __pulse_fetch_collect_failure(effectIndex)
82
+ return value > 0 ? __pulse_fetch_envelope(effectIndex, value) : 0
83
+ }
84
+ `;
85
+ return source;
86
+ }
87
+ module.exports = { instrument };
@@ -0,0 +1,74 @@
1
+ // Optional transport observations reuse the fetch driver's existing wait set.
2
+ @external('fastly_async_io', 'is_ready') declare function __pulse_timing_ready(handle:i32,out:usize):i32
3
+ const __pulse_timing_starts = new StaticArray<i64>(PULSE_FASTLY_EFFECT_COUNT)
4
+ const __pulse_timing_ends = new StaticArray<i64>(PULSE_FASTLY_EFFECT_COUNT)
5
+ const __pulse_timing_clock_out = new StaticArray<i64>(1)
6
+ const __pulse_timing_io = new StaticArray<i32>(1)
7
+ class __PulseTimedBody {
8
+ value:i32; bytes:Uint8Array; total:i32=0; done:bool=false; last:i64=-2;
9
+ constructor(value:i32) { this.value=value; this.bytes=new Uint8Array(__PULSE_SCHEMA_MAX_BYTES+1); }
10
+ }
11
+ const __pulse_timing_bodies = new Array<__PulseTimedBody|null>(PULSE_FASTLY_EFFECT_COUNT)
12
+ function __pulse_timing_selected(index:i32):bool { return /*TIMED*/ }
13
+ function __pulse_timing_projected(index:i32):bool { return /*PROJECTED*/ }
14
+ function __pulse_timing_clock():i64 {
15
+ if(wasi_snapshot_preview1_clock_time_get(1,1,changetype<usize>(__pulse_timing_clock_out))!=0)return -1;
16
+ return __pulse_timing_clock_out[0]>=0?__pulse_timing_clock_out[0]:-1;
17
+ }
18
+ function __pulse_timing_reset(index:i32):void {
19
+ __pulse_timing_starts[index]=-1; __pulse_timing_ends[index]=-2; __pulse_timing_bodies[index]=null;
20
+ }
21
+ function __pulse_timing_begin(index:i32):void { if(__pulse_timing_selected(index))__pulse_timing_starts[index]=__pulse_timing_clock(); }
22
+ function __pulse_timing_end(index:i32):void { if(__pulse_timing_selected(index)&&__pulse_timing_ends[index]==-2)__pulse_timing_ends[index]=__pulse_timing_clock(); }
23
+ function __pulse_timing_elapsed(index:i32):i32 {
24
+ const a=__pulse_timing_starts[index],b=__pulse_timing_ends[index];
25
+ return a<0||b<a?host_value_null():host_value_number(f64(b-a)/1000000.0);
26
+ }
27
+ function __pulse_timing_active():bool {
28
+ for(let i=0;i<PULSE_FASTLY_EFFECT_COUNT;i++) {
29
+ if(__pulse_timing_selected(i)&&__pulse_fetch_active(i))return true;
30
+ const row=__pulse_timing_bodies[i];if(row!==null&&!row.done)return true;
31
+ }
32
+ return false;
33
+ }
34
+ function __pulse_timing_headers(index:i32,value:i32):void {
35
+ if(!__pulse_timing_projected(index)){__pulse_fetch_complete(index,0,value);return;}
36
+ if(__PULSE_SCHEMA_MAX_BYTES<=0||__PULSE_SCHEMA_MAX_BYTES>1073741823){__pulse_fastly_fail(PULSE_ERROR_SCHEMA,21,index);return;}
37
+ // The member timeout ends at headers; enclosing authority still fences reads.
38
+ __pulse_fetch_timeout[index]=0;
39
+ __pulse_fastly_pending_mode[index]=PULSE_FASTLY_PENDING_READY;
40
+ __pulse_fastly_pending[index]=0;
41
+ __pulse_timing_bodies[index]=new __PulseTimedBody(value);
42
+ }
43
+ function __pulse_timing_read_round():void {
44
+ const out=__pulse_timing_io;
45
+ for(let i=0;i<PULSE_FASTLY_EFFECT_COUNT;i++) {
46
+ const row=__pulse_timing_bodies[i]; if(row===null||row.done)continue;
47
+ if(!__pulse_fetch_request_check(i))return;
48
+ const value=__pulse_fastly_value(row.value);
49
+ out[0]=-1;
50
+ if(__pulse_timing_ready(value.bodyHandle,changetype<usize>(out))!=0||out[0]<0||out[0]>1){__pulse_fastly_fail(PULSE_ERROR_HOSTCALL,272,i);return;}
51
+ if(out[0]==0)continue;
52
+ const capacity=min<i32>(PULSE_FASTLY_BUFFER_BYTES,row.bytes.length-row.total);
53
+ out[0]=-1;
54
+ const status=fastly_http_body_read(value.bodyHandle,row.bytes.dataStart+row.total,capacity,changetype<usize>(out));
55
+ if(status!=0){row.done=true;__pulse_fetch_complete(i,status);continue;}
56
+ if(out[0]<0||out[0]>capacity){__pulse_fastly_fail(PULSE_ERROR_HOSTCALL,20,i);return;}
57
+ if(out[0]==0){row.done=true;if(row.last!=-2)__pulse_timing_ends[i]=row.last;__pulse_fetch_complete(i,0,row.value);continue;}
58
+ const sample=__pulse_timing_clock();
59
+ row.last=row.last==-1||sample<row.last?-1:sample;
60
+ row.total+=out[0];
61
+ if(row.total>__PULSE_SCHEMA_MAX_BYTES){__pulse_fastly_fail(PULSE_ERROR_SCHEMA,21,i);return;}
62
+ }
63
+ }
64
+ function __pulse_timing_decode(value:__PulseFastlyValue):bool {
65
+ if(value.bodyLoaded!=0)return true;
66
+ for(let i=0;i<PULSE_FASTLY_EFFECT_COUNT;i++) {
67
+ const row=__pulse_timing_bodies[i];
68
+ if(row!==null&&row.done&&__pulse_fastly_value(row.value)===value){
69
+ value.text=__pulse_fastly_decode(row.bytes,row.total);value.bodyLoaded=1;
70
+ __pulse_timing_bodies[i]=null;return true;
71
+ }
72
+ }
73
+ return false;
74
+ }
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ function instrument(input, plan) {
5
+ const timed = plan.effects.flatMap((e,i)=>e.kind==='fetch' && e.fetchTiming ? [i] : []);
6
+ const projected = timed.filter(i=>plan.effects[i].decoder);
7
+ let source = input;
8
+ const replace = (a,b) => { if(source.split(a).length!==2)throw Error('Fetch timing boundary drift: '+a);source=source.replace(a,b); };
9
+ const selected = index => timed.map(i=>`${index} == ${i}`).join(' || ') || 'false';
10
+ // Dynamic init must not introduce optional instrumentation after compilation.
11
+ const anchor = 'headers.kind = PULSE_VALUE_OBJECT; if (init.kind == PULSE_VALUE_OBJECT) {';
12
+ replace(anchor, anchor+`
13
+ const timingIndex = __pulse_fastly_find(init, 'timing');
14
+ const timing = timingIndex >= 0 ? __pulse_fastly_value(init.values[timingIndex]) : new __PulseFastlyValue();
15
+ if ((timingIndex >= 0 && timing.kind != PULSE_VALUE_UNDEFINED && timing.kind != PULSE_VALUE_BOOLEAN)
16
+ || (timing.kind == PULSE_VALUE_BOOLEAN && timing.boolean) != (${selected('effectIndex')})) {
17
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 48, effectIndex); return;
18
+ }
19
+ `);
20
+ if(!timed.length)return source;
21
+ replace('function __pulse_fetch_begin_deadline(index: i32, init: __PulseFastlyValue): void {',
22
+ 'function __pulse_fetch_begin_deadline(index: i32, init: __PulseFastlyValue): void { __pulse_timing_reset(index);');
23
+ replace('status = fastly_http_req_send_async(requestHandle, bodyHandle,',
24
+ '__pulse_timing_begin(effectIndex); status = fastly_http_req_send_async(requestHandle, bodyHandle,');
25
+ replace('function __pulse_fetch_has_deadlines(): bool {',
26
+ 'function __pulse_fetch_has_deadlines(): bool { if (__pulse_timing_active()) return true;');
27
+ replace('function __pulse_fetch_complete(index: i32, outcome: i32, result: i32 = 0): void {',
28
+ 'function __pulse_fetch_complete(index: i32, outcome: i32, result: i32 = 0): void { __pulse_timing_end(index);');
29
+ replace('__pulse_fetch_complete(i, 0, __pulse_fastly_put(value)); continue',
30
+ '__pulse_timing_headers(i, __pulse_fastly_put(value)); continue');
31
+ replace('while (!__pulse_fetch_completed[index]) {',
32
+ 'while (!__pulse_fetch_completed[index] || __pulse_timing_active()) {');
33
+ // Both early-return fences must wait for transport observations, never decoding.
34
+ source=source.replaceAll('if (__pulse_fetch_completed[index]) break',
35
+ 'if (__pulse_fetch_completed[index] && !__pulse_timing_active()) break');
36
+ replace(' if (__pulse_fetch_completed[index] && !__pulse_timing_active()) break\n if (!__pulse_fetch_request_check(index))',
37
+ ' __pulse_timing_read_round(); if (__pulse_fastly_last_error != 0) return 0;\n if (__pulse_fetch_completed[index] && !__pulse_timing_active()) break\n if (!__pulse_fetch_request_check(index))');
38
+ // Add active bodies to the same wait set. One pending request OR body per site.
39
+ replace(' remaining = Math.min(remaining, f64(__pulse_fetch_request_remaining()))',
40
+ ' for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) { const row = __pulse_timing_bodies[i]; if (row !== null && !row.done) handles[count++] = __pulse_fastly_value(row.value).bodyHandle; }\n remaining = Math.min(remaining, f64(__pulse_fetch_request_remaining()))');
41
+ replace('function __pulse_fastly_fetch_body(value: __PulseFastlyValue): string {',
42
+ 'function __pulse_fastly_fetch_body(value: __PulseFastlyValue): string { if (__pulse_timing_decode(value)) return value.text;');
43
+ replace('const failure = __pulse_fetch_collect_failure(effectIndex)',
44
+ '__pulse_timing_end(effectIndex); const failure = __pulse_fetch_collect_failure(effectIndex)');
45
+ source += fs.readFileSync(path.join(__dirname,'native-fetch-timing.as.ts'),'utf8')
46
+ .replace('/*TIMED*/',selected('index'))
47
+ .replace('/*PROJECTED*/',projected.map(i=>`index == ${i}`).join(' || ') || 'false');
48
+ return source;
49
+ }
50
+ module.exports = { instrument };