@pulse-compute/provider-fastly 0.0.0 → 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 (103) hide show
  1. package/README.md +53 -1
  2. package/dist/config-schema.json +103 -0
  3. package/dist/index.d.ts +77 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +67 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +88 -5
  8. package/src/build/canonical-target.js +469 -0
  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 +559 -0
  13. package/src/build/fixed-memory-runtime.as.ts +148 -0
  14. package/src/build/kv-native.as.ts +331 -0
  15. package/src/build/kv-native.js +27 -0
  16. package/src/build/native-application-errors.js +159 -0
  17. package/src/build/native-binary-resources.js +30 -0
  18. package/src/build/native-body-forwarding.as.ts +341 -0
  19. package/src/build/native-body-forwarding.js +69 -0
  20. package/src/build/native-dynamic-backends.as.ts +115 -0
  21. package/src/build/native-dynamic-backends.js +52 -0
  22. package/src/build/native-fetch-deadlines.as.ts +164 -0
  23. package/src/build/native-fetch-deadlines.js +27 -0
  24. package/src/build/native-fetch-errors.js +38 -0
  25. package/src/build/native-handle-ownership.js +127 -0
  26. package/src/build/native-header-lookup.js +50 -0
  27. package/src/build/native-http-effects.js +994 -0
  28. package/src/build/native-http-shell.js +703 -0
  29. package/src/build/native-platform-capabilities.js +2654 -0
  30. package/src/build/native-request-body.js +44 -0
  31. package/src/build/native-request-headers.js +93 -0
  32. package/src/build/native-send-error-detail.as.ts +12 -0
  33. package/src/build/native-stdio.js +45 -0
  34. package/src/build/native-string-values.js +143 -0
  35. package/src/build/native-transport-details.as.ts +84 -0
  36. package/src/build/native-value-budget.js +90 -0
  37. package/src/build/package-native-application.js +114 -0
  38. package/src/build/request-budget.as.ts +50 -0
  39. package/src/build/request-budget.js +47 -0
  40. package/src/build/s3-body.as.ts +97 -0
  41. package/src/build/s3-native.as.ts +228 -0
  42. package/src/build/s3-native.js +30 -0
  43. package/src/build/schema-admission.js +83 -0
  44. package/src/build/schema-nested-json.js +93 -0
  45. package/src/build/schema-scalar-record.js +41 -0
  46. package/src/build/time-native.js +49 -0
  47. package/src/build/wasm-budget.js +28 -0
  48. package/src/compiler/assets-package-out-parity-proof.js +338 -0
  49. package/src/compiler/assets-provider-proof.js +499 -0
  50. package/src/compiler/config-provider-resolution.js +573 -0
  51. package/src/compiler/fastly-adapter.js +564 -0
  52. package/src/compiler/fastly-command-entry.js +390 -0
  53. package/src/compiler/fastly-config-secret-serve.js +323 -0
  54. package/src/compiler/fastly-hostcall-binding.js +598 -0
  55. package/src/compiler/fastly-readiness.js +477 -0
  56. package/src/compiler/index.js +40 -0
  57. package/src/compiler/lifecycle-parity-contract.js +70 -0
  58. package/src/compiler/lifecycle-parity-proof.js +260 -0
  59. package/src/compiler/route-handler-effect-proof.js +255 -0
  60. package/src/compiler/runtime-provider-kv.js +237 -0
  61. package/src/compiler/runtime-provider-shape.js +109 -0
  62. package/src/config-api.js +69 -0
  63. package/src/config-schema.d.ts +13 -0
  64. package/src/config-schema.js +27 -0
  65. package/src/config-schema.json +103 -0
  66. package/src/index.js +3 -0
  67. package/src/index.ts +143 -0
  68. package/src/javascript/assets-adapter.js +122 -0
  69. package/src/javascript/bindings-adapter.js +237 -0
  70. package/src/javascript/errors.js +26 -0
  71. package/src/javascript/fetch-adapter.js +93 -0
  72. package/src/javascript/grip-broadcast.js +261 -0
  73. package/src/javascript/jwt-verifier.js +147 -0
  74. package/src/javascript/lifecycle.js +55 -0
  75. package/src/javascript/local-loader.js +138 -0
  76. package/src/javascript/logging.js +35 -0
  77. package/src/javascript/package-effects.js +71 -0
  78. package/src/javascript/runtime-host.js +112 -0
  79. package/src/javascript/sha256.js +128 -0
  80. package/src/javascript/source-package.js +778 -0
  81. package/src/javascript/support.js +142 -0
  82. package/src/javascript/target-support-policy.js +277 -0
  83. package/src/javascript/target.js +89 -0
  84. package/src/javascript/test-runtime.js +408 -0
  85. package/src/provider-contract.js +88 -0
  86. package/src/runtime/assets-provider.js +464 -0
  87. package/src/runtime/canonical-api-runtime.js +603 -0
  88. package/src/runtime/index.js +28 -0
  89. package/src/runtime/provider-primitives-contract.js +272 -0
  90. package/src/runtime/provider-primitives.js +82 -0
  91. package/src/runtime/route-handler-effects.js +507 -0
  92. package/src/testing/conditional-kv-host.js +155 -0
  93. package/src/testing/fastly-cli.js +538 -0
  94. package/src/testing/native-dynamic-backends-host.js +39 -0
  95. package/src/testing/native-http-effects-host.js +398 -0
  96. package/src/testing/native-http-shell-host.js +190 -0
  97. package/src/testing/native-platform-capabilities-host.js +924 -0
  98. package/src/toolchain/body-forwarding.js +25 -0
  99. package/src/toolchain/config.js +144 -0
  100. package/src/toolchain/index.js +386 -0
  101. package/src/toolchain/logging.js +18 -0
  102. package/src/toolchain/native-execution.js +78 -0
  103. package/src/toolchain/s3.js +26 -0
@@ -0,0 +1,115 @@
1
+ // Provider-owned, request-local dynamic backend policy. No optional timeout or
2
+ // trust overrides: deadlines and certificate validation retain their owners.
3
+ @external("fastly_http_req", "register_dynamic_backend")
4
+ declare function __pulse_dynamic_register(name: usize, nameLength: i32, target: usize, targetLength: i32, mask: u32, config: usize): i32
5
+ const __pulse_dynamic_origins = new Array<string>()
6
+ const __pulse_dynamic_names = new Array<string>()
7
+ let __pulse_dynamic_attempts: i32 = 0
8
+ let __pulse_dynamic_status: i32 = 0
9
+ let __pulse_dynamic_url: string = ""
10
+ export function pulse_fastly_dynamic_backend_status(): i32 { return __pulse_dynamic_status }
11
+ function __pulse_dynamic_clear(): void {
12
+ __pulse_dynamic_origins.length = 0; __pulse_dynamic_names.length = 0; __pulse_dynamic_url = ""
13
+ }
14
+ function __pulse_dynamic_invalid(index: i32): string {
15
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 273, index); return ""
16
+ }
17
+ function __pulse_dynamic_select(index: i32, url: string): string {
18
+ __pulse_dynamic_url = url
19
+ const fixed = __pulse_fastly_backend(index)
20
+ if (fixed.length > 0) return fixed
21
+ if (!__pulse_dynamic_check(index)) return ""
22
+ if (url.length > PULSE_FASTLY_BUFFER_BYTES) return __pulse_dynamic_invalid(index)
23
+ for (let i = 0; i < url.length; i++) {
24
+ const c = url.charCodeAt(i)
25
+ if (c <= 32 || c >= 127 || c == 92) return __pulse_dynamic_invalid(index)
26
+ }
27
+ const separator = url.indexOf("://")
28
+ if (separator < 0) return __pulse_dynamic_invalid(index)
29
+ const scheme = url.substring(0, separator).toLowerCase()
30
+ const tls = scheme == "https"
31
+ if (!tls && scheme != "http") return __pulse_dynamic_invalid(index)
32
+ const start = separator + 3
33
+ let end = start
34
+ while (end < url.length && url.charAt(end) != "/" && url.charAt(end) != "?" && url.charAt(end) != "#") end++
35
+ const authority = url.substring(start, end).toLowerCase()
36
+ if (authority.length == 0 || authority.length > 1024 || authority.indexOf("@") >= 0) return __pulse_dynamic_invalid(index)
37
+ const colon = authority.indexOf(":")
38
+ let hostname = colon < 0 ? authority : authority.substring(0, colon)
39
+ let port = tls ? 443 : 80
40
+ if (colon >= 0) {
41
+ const digits = authority.substring(colon + 1)
42
+ if (digits.length == 0 || digits.length > 5) return __pulse_dynamic_invalid(index)
43
+ port = 0
44
+ for (let i = 0; i < digits.length; i++) {
45
+ const c = digits.charCodeAt(i)
46
+ if (c < 48 || c > 57) return __pulse_dynamic_invalid(index)
47
+ port = port * 10 + c - 48
48
+ }
49
+ if (port < 1 || port > 65535) return __pulse_dynamic_invalid(index)
50
+ }
51
+ const dns = hostname.endsWith(".") ? hostname.substring(0, hostname.length - 1) : hostname
52
+ if (dns.length == 0 || dns.length > 253) return __pulse_dynamic_invalid(index)
53
+ const labels = dns.split(".")
54
+ let allNumeric = true
55
+ for (let i = 0; i < labels.length; i++) {
56
+ const label = unchecked(labels[i])
57
+ if (label.length == 0 || label.length > 63 || label.startsWith("xn--") || label.startsWith("-") || label.endsWith("-")) return __pulse_dynamic_invalid(index)
58
+ for (let j = 0; j < label.length; j++) {
59
+ const c = label.charCodeAt(j)
60
+ if (!((c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c == 45)) return __pulse_dynamic_invalid(index)
61
+ if (c < 48 || c > 57) allNumeric = false
62
+ }
63
+ }
64
+ if (allNumeric) {
65
+ if (tls || hostname != dns || labels.length != 4) return __pulse_dynamic_invalid(index)
66
+ for (let i = 0; i < 4; i++) {
67
+ const label = unchecked(labels[i])
68
+ if (label.length > 3 || (label.length > 1 && label.startsWith("0"))) return __pulse_dynamic_invalid(index)
69
+ let octet = 0
70
+ for (let j = 0; j < label.length; j++) octet = octet * 10 + label.charCodeAt(j) - 48
71
+ if (octet > 255) return __pulse_dynamic_invalid(index)
72
+ }
73
+ } else {
74
+ // WHATWG numeric host aliases must not become a different DNS destination.
75
+ const last = unchecked(labels[labels.length - 1])
76
+ let numeric = true
77
+ for (let i = 0; i < last.length; i++) if (last.charCodeAt(i) < 48 || last.charCodeAt(i) > 57) numeric = false
78
+ if (numeric || last.startsWith("0x")) return __pulse_dynamic_invalid(index)
79
+ }
80
+ const host = hostname + (port == (tls ? 443 : 80) ? "" : ":" + port.toString())
81
+ const origin = scheme + "://" + host
82
+ let rest = url.substring(end)
83
+ const fragment = rest.indexOf("#")
84
+ if (fragment >= 0) rest = rest.substring(0, fragment)
85
+ if (!rest.startsWith("/")) rest = "/" + rest
86
+ __pulse_dynamic_url = origin + rest
87
+ if (__pulse_dynamic_url.length > PULSE_FASTLY_BUFFER_BYTES) return __pulse_dynamic_invalid(index)
88
+ const mapped = __pulse_dynamic_static(origin)
89
+ if (mapped.length > 0) return mapped
90
+ for (let i = 0; i < __pulse_dynamic_origins.length; i++) {
91
+ if (unchecked(__pulse_dynamic_origins[i]) == origin) return unchecked(__pulse_dynamic_names[i])
92
+ }
93
+ if (__pulse_dynamic_attempts >= 32) {
94
+ __pulse_fastly_fail(PULSE_ERROR_VALUE, 276, index); return ""
95
+ }
96
+ const name = "pulse-native-dynamic-" + (__pulse_dynamic_attempts++).toString()
97
+ const n = __pulse_fastly_utf8(name), target = __pulse_fastly_utf8(hostname + ":" + port.toString())
98
+ const h = __pulse_fastly_utf8(host), sni = __pulse_fastly_utf8(hostname)
99
+ const config = new StaticArray<u32>(27)
100
+ config[0] = <u32>changetype<usize>(h); config[1] = h.byteLength
101
+ let mask: u32 = 2
102
+ if (tls) {
103
+ mask |= 32 | 256 | 2048
104
+ config[7] = <u32>changetype<usize>(sni); config[8] = sni.byteLength
105
+ config[13] = <u32>changetype<usize>(sni); config[14] = sni.byteLength
106
+ }
107
+ if (!__pulse_dynamic_check(index)) return ""
108
+ __pulse_dynamic_status = __pulse_dynamic_register(changetype<usize>(n), n.byteLength, changetype<usize>(target), target.byteLength, mask, changetype<usize>(config))
109
+ if (!__pulse_dynamic_check(index)) return ""
110
+ if (__pulse_dynamic_status != 0) {
111
+ __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, __pulse_dynamic_status == 5 ? 274 : 275, index); return ""
112
+ }
113
+ __pulse_dynamic_origins.push(origin); __pulse_dynamic_names.push(name)
114
+ return name
115
+ }
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const policy = Object.freeze({
6
+ optIn: 'bindings.dynamicBackends', maxRegistrationAttempts: 32,
7
+ identity: 'canonical origin with derived Host/TLS policy; request-local',
8
+ precedence: 'effect backend, configured canonical origin, dynamic registration',
9
+ urls: 'absolute ASCII HTTP/HTTPS DNS; canonical IPv4 over HTTP; ports 1–65535; no credentials, IDNA or IPv6',
10
+ tls: 'platform trust; URL DNS hostname for certificate verification and SNI',
11
+ hostPrerequisite: 'Fastly Dynamic Backends service capability',
12
+ stages: Object.freeze({ invalidUrl: 273, unavailable: 274, registrationFailed: 275, attemptsExhausted: 276 })
13
+ });
14
+ function active(bindings) { return (Array.isArray(bindings) ? bindings : bindings.backends || []).some(b => b.dynamic === true); }
15
+ function originMappings(origins) {
16
+ return Object.fromEntries(Object.entries(origins || {}).filter(([, name]) => typeof name === 'string' && name.trim()).map(([origin, name]) => [origin, name.trim()]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
17
+ }
18
+ function describe(origins) { return Object.freeze({ ...policy, staticOrigins: Object.freeze(originMappings(origins)) }); }
19
+ function runtimeSource(backends, platform, origins) {
20
+ if (!active(backends)) return '';
21
+ const mappings = Object.entries(originMappings(origins));
22
+ return fs.readFileSync(path.join(__dirname, 'native-dynamic-backends.as.ts'), 'utf8') + '\n'
23
+ + `function __pulse_dynamic_static(origin: string): string {\n${mappings.map(([origin, name]) => ` if (origin == ${JSON.stringify(origin)}) return ${JSON.stringify(name.trim())}`).join('\n')}\n return ""\n}\n`
24
+ + `function __pulse_dynamic_check(index: i32): bool { return ${platform ? '__pulse_fetch_request_check(index) && __pulse_fetch_before_send(index)' : 'true'} }\n`;
25
+ }
26
+ function instrument(source, bindings, platform, origins) {
27
+ if (!active(bindings)) return source;
28
+ const replace = (before, after) => {
29
+ if (!source.includes(before)) throw new Error('Fastly dynamic backend boundary drift: ' + before.slice(0, 100));
30
+ source = source.replace(before, after);
31
+ };
32
+ if (platform) {
33
+ replace('__pulse_fastly_start_request(effectIndex, url, method, headers, body, __pulse_fastly_backend(effectIndex))',
34
+ 'const backend = __pulse_dynamic_select(effectIndex, url); if (__pulse_fastly_last_error != 0) return; __pulse_fastly_start_request(effectIndex, __pulse_dynamic_url, method, headers, body, backend)');
35
+ } else {
36
+ replace('function host_effect_begin(effectIndex: i32, payload: i32): void {',
37
+ 'function host_effect_begin(effectIndex: i32, payload: i32): void { if (__pulse_fastly_last_error != 0) return;');
38
+ replace(' const requestOut = __pulse_fastly_out_i32(); let status = fastly_http_req_new',
39
+ ' const backend = __pulse_dynamic_select(effectIndex, url); if (__pulse_fastly_last_error != 0) return;\n const requestOut = __pulse_fastly_out_i32(); let status = fastly_http_req_new');
40
+ replace('const urlBytes = __pulse_fastly_utf8(url); status = fastly_http_req_uri_set',
41
+ 'const urlBytes = __pulse_fastly_utf8(__pulse_dynamic_url); status = fastly_http_req_uri_set');
42
+ replace('const backend = __pulse_fastly_backend(effectIndex); if (backend.length == 0)', 'if (backend.length == 0)');
43
+ }
44
+ const entry = source.includes('export function pulse_fastly_handle(): void {') ? 'pulse_fastly_handle' : '_start';
45
+ replace(`export function ${entry}(): void {`, 'function __pulse_dynamic_start(): void {');
46
+ return source + '\n' + runtimeSource(bindings, platform, origins) + `\nexport function ${entry}(): void {
47
+ __pulse_dynamic_clear(); __pulse_dynamic_attempts = 0; __pulse_dynamic_status = 0
48
+ __pulse_dynamic_start()
49
+ __pulse_dynamic_clear()
50
+ }\n`;
51
+ }
52
+ module.exports = { active, policy, describe, instrument };
@@ -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};