@pulse-compute/provider-fastly 1.0.0-beta.8 → 1.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 };
@@ -519,6 +519,10 @@ class __PulseJsonParser {
519
519
  }
520
520
 
521
521
  const __pulse_fastly_values = new Array<__PulseFastlyValue>()
522
+ // Shared schema admission uses storage-independent lookups. This retained
523
+ // HTTP emitter keeps its existing dense table and lifetime policy.
524
+ function __pulse_fastly_has_value(handle: i32): bool { return handle > 0 && handle <= __pulse_fastly_values.length }
525
+ function __pulse_fastly_value_count(): i32 { return __pulse_fastly_values.length }
522
526
  const __pulse_fastly_pending = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
523
527
  const __pulse_fastly_pending_active = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
524
528
  const __pulse_fastly_payloads = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { scalarRecordProjectionLines } = require('./schema-scalar-record.js');
4
4
  const { hasJsonAdmission, tracksJsonDuplicates, schemaParserDepth, nestedJsonProjectionLines, schemaJsonAdmissionSource, schemaFetchJsonSource } = require('./schema-nested-json.js');
5
+ const { nativeStdioSource } = require('./native-stdio.js');
5
6
  const applicationErrors = require('./native-application-errors.js');
6
7
  const effectInvocations = require('./effect-invocations.js');
7
8
  const packageApplication = require('./package-native-application.js');
@@ -88,6 +89,7 @@ function loadGuestToolchain() {
88
89
 
89
90
  const { resolveAsc } = loadBuildSupport();
90
91
  const { generateCanonicalNativeAssemblyScript } = loadNativeGenerator();
92
+ const nativeHandleOwnership = require('./native-handle-ownership.js');
91
93
  const { pulseJwtAssemblyScriptSource } = loadJwtNativeSource();
92
94
  const {
93
95
  GUEST_LINK_STAGE_INVOCATION_VERSION,
@@ -108,6 +110,7 @@ const FASTLY_NATIVE_PLATFORM_CAPABILITIES_BUFFER_BYTES = 65536;
108
110
  const FASTLY_NATIVE_SIZE_OPTIMIZATION = EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION;
109
111
 
110
112
  const FASTLY_NATIVE_PLATFORM_CAPABILITIES_IMPORTS = Object.freeze([
113
+ ...require('./native-body-forwarding.js').IMPORTS.map(key => Object.freeze(key.split(':'))),
111
114
  ...conditionalKv.KV_IMPORTS.map((key) => Object.freeze(key.split(':'))),
112
115
  ...require('./s3-native.js').S3_IMPORTS.map((key) => Object.freeze(key.split(':'))),
113
116
  Object.freeze(['fastly_abi', 'init']),
@@ -143,8 +146,7 @@ const FASTLY_NATIVE_PLATFORM_CAPABILITIES_IMPORTS = Object.freeze([
143
146
  Object.freeze(['fastly_kv_store', 'lookup_wait_v2']),
144
147
  Object.freeze(['fastly_kv_store', 'insert']),
145
148
  Object.freeze(['fastly_kv_store', 'insert_wait']),
146
- Object.freeze(['fastly_log', 'endpoint_get']),
147
- Object.freeze(['fastly_log', 'write']),
149
+ Object.freeze(['wasi_snapshot_preview1', 'fd_write']),
148
150
  Object.freeze(['wasi_snapshot_preview1', 'clock_time_get'])
149
151
  ].filter(([module, name], index, entries) => entries.findIndex((item) => item[0] === module && item[1] === name) === index));
150
152
 
@@ -663,6 +665,7 @@ function fastlyGuestLinkedStartWrapperWat() {
663
665
  }
664
666
 
665
667
  function validatePlanBoundary(plan, options = {}) {
668
+ require('./native-body-forwarding.js').validate(plan, options);
666
669
  if (!plan || typeof plan !== 'object') throw new TypeError('Fastly native platform capability realization requires a canonical native plan.');
667
670
  if (plan.version !== CANONICAL_NATIVE_PLAN_VERSION) {
668
671
  fail(`Fastly native platform capability realization requires ${CANONICAL_NATIVE_PLAN_VERSION}.`, 'PULSE_FASTLY_NATIVE_PLATFORM_CAPABILITIES_PLAN_VERSION_UNSUPPORTED', { version: plan.version });
@@ -930,6 +933,7 @@ function resolveCapabilityBindings(plan, options = {}) {
930
933
 
931
934
  return Object.freeze({
932
935
  maxDurationMs: require('@pulse-compute/runtime/host').normalizeRequestDuration(options.maxDurationMs),
936
+ bodyForwarding: require('../toolchain/body-forwarding.js').normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs),
933
937
  configStore,
934
938
  secretStore,
935
939
  s3: require('../toolchain/s3.js').resolveFastlyS3(effects.map((effect) => ({ ...effect, payload: Object.fromEntries(['contentType', 'method', 'range', 'ifNoneMatch'].map(name => [name, literalObjectField(inputExpression(effect, 'payload'), name) ?? undefined])) })), source.s3),
@@ -1009,10 +1013,14 @@ function requiredImportsForPlan(plan, bindings) {
1009
1013
  if (kinds.has('kv.put')) for (const key of ['fastly_kv_store:insert', 'fastly_kv_store:insert_wait']) keys.add(key);
1010
1014
  if (kinds.has('grip.hold')) keys.add('fastly_http_resp:header_append');
1011
1015
  if (plan.logging && plan.logging.enabledStatements > 0) {
1012
- keys.add('fastly_log:endpoint_get');
1013
- keys.add('fastly_log:write');
1016
+ keys.add('wasi_snapshot_preview1:fd_write');
1014
1017
  }
1015
1018
  if (kinds.has('s3.head') || kinds.has('s3.getText') || kinds.has('s3.putText') || kinds.has('s3.getBody')) for (const key of require('./s3-native.js').S3_IMPORTS) keys.add(key);
1019
+ if (require('./native-body-forwarding.js').enabled(plan)) {
1020
+ keys.delete('fastly_http_req:send_async'); keys.delete('fastly_http_req:pending_req_wait');
1021
+ for (const key of require('./native-body-forwarding.js').IMPORTS) keys.add(key);
1022
+ keys.add('fastly_http_body:read');
1023
+ }
1016
1024
  return Object.freeze([...keys].sort());
1017
1025
  }
1018
1026
 
@@ -1435,8 +1443,7 @@ function __pulse_fastly_jwt_send_error(): void {}
1435
1443
  @external("fastly_kv_store", "lookup_wait_v2") declare function fastly_kv_store_lookup_wait_v2(handle: i32, bodyOut: usize, metadata: usize, metadataLength: i32, writtenOut: usize, generationOut: usize, kvErrorOut: usize): i32
1436
1444
  @external("fastly_kv_store", "insert") declare function fastly_kv_store_insert(store: i32, key: usize, keyLength: i32, body: i32, configMask: i32, config: usize, insertOut: usize): i32
1437
1445
  @external("fastly_kv_store", "insert_wait") declare function fastly_kv_store_insert_wait(handle: i32, kvErrorOut: usize): i32
1438
- @external("fastly_log", "endpoint_get") declare function fastly_log_endpoint_get(name: usize, nameLength: i32, handleOut: usize): i32
1439
- @external("fastly_log", "write") declare function fastly_log_write(handle: i32, message: usize, messageLength: i32, writtenOut: usize): i32
1446
+ ${nativeStdioSource}
1440
1447
  @external("wasi_snapshot_preview1", "clock_time_get") declare function wasi_snapshot_preview1_clock_time_get(clockId: i32, precision: i64, timeOut: usize): i32
1441
1448
 
1442
1449
  const PULSE_FASTLY_EFFECTS_ABI_VERSION: i64 = ${FASTLY_NATIVE_PLATFORM_CAPABILITIES_ABI_VERSION}
@@ -1508,6 +1515,7 @@ ${nativeStringFields}
1508
1515
  }
1509
1516
  binaryBody: Uint8Array | null = null
1510
1517
  immutable: bool = false
1518
+ ${nativeHandleOwnership.fields(options.handleTracing)}
1511
1519
  }
1512
1520
  class __PulseFastlyHeader {
1513
1521
  constructor(public name: string, public value: string) {}
@@ -1640,7 +1648,7 @@ class __PulseJsonParser {
1640
1648
  const PULSE_FASTLY_PENDING_NONE: i32 = 0
1641
1649
  const PULSE_FASTLY_PENDING_ASYNC: i32 = 1
1642
1650
  const PULSE_FASTLY_PENDING_READY: i32 = 2
1643
- @lazy const __pulse_fastly_values = new Array<__PulseFastlyValue>()
1651
+ ${nativeHandleOwnership.tableSource(options.handleTracing)}
1644
1652
  // Primitive handles have value semantics. Bounded caches keep admitted scalar
1645
1653
  // validation loops from retaining a fresh boxed value for every iteration.
1646
1654
  @lazy const __pulse_fastly_scalar_handles = new StaticArray<i32>(4)
@@ -1664,7 +1672,6 @@ let __pulse_fastly_request_body_loaded: i32 = 0
1664
1672
  let __pulse_fastly_last_error: i32 = 0
1665
1673
  let __pulse_fastly_error_stage: i32 = 0
1666
1674
  let __pulse_fastly_error_effect: i32 = -1
1667
- let __pulse_fastly_log_handle: i32 = -1
1668
1675
  let __pulse_fastly_jwt_error: i32 = 0
1669
1676
  let __pulse_fastly_jwt_secret_calls: i32 = 0
1670
1677
  let __pulse_fastly_jwt_crypto_calls: i32 = 0
@@ -1681,8 +1688,6 @@ function __pulse_fastly_out_i32(): StaticArray<i32> { return new StaticArray<i32
1681
1688
  function __pulse_fastly_out_value(out: StaticArray<i32>): i32 { return load<i32>(changetype<usize>(out)) }
1682
1689
  function __pulse_fastly_utf8(value: string): ArrayBuffer { return String.UTF8.encode(value, false) }
1683
1690
  function __pulse_fastly_decode(buffer: Uint8Array, length: i32): string { return String.UTF8.decodeUnsafe(buffer.dataStart, length, false) }
1684
- function __pulse_fastly_put(value: __PulseFastlyValue): i32 { __pulse_fastly_values.push(value); return __pulse_fastly_values.length }
1685
- function __pulse_fastly_value(handle: i32): __PulseFastlyValue { if (handle <= 0 || handle > __pulse_fastly_values.length) { __pulse_fastly_fail(PULSE_ERROR_VALUE, 1, -1); return new __PulseFastlyValue() } return unchecked(__pulse_fastly_values[handle - 1]) }
1686
1691
  ${nativeStringConcat}
1687
1692
  ${nativeStringTrim}
1688
1693
  ${nativeStringIndex}
@@ -1692,7 +1697,7 @@ function __pulse_fastly_string_value(value: string): i32 {
1692
1697
  if (index >= 0) { const cached = unchecked(__pulse_fastly_character_handles[index]); if (cached != 0) return cached }
1693
1698
  const item = new __PulseFastlyValue(); item.kind = PULSE_VALUE_STRING; item.text = value;
1694
1699
  const handle = __pulse_fastly_put(item);
1695
- if (index >= 0) unchecked(__pulse_fastly_character_handles[index] = handle);
1700
+ if (index >= 0) { unchecked(__pulse_fastly_character_handles[index] = handle); ${options.handleTracing ? 'if (handle != 0) __pulse_handle_cache_roots.push(handle);' : ''} }
1696
1701
  return handle
1697
1702
  }
1698
1703
  function __pulse_fastly_find(object: __PulseFastlyValue, key: string): i32 { for (let i = 0; i < object.keys.length; i += 1) if (unchecked(object.keys[i]) == key) return i; return -1 }
@@ -1723,7 +1728,7 @@ function __pulse_fastly_redact_fields(input: string): string {
1723
1728
  return output + input.substring(start);
1724
1729
  }
1725
1730
  function __pulse_fastly_redact(input: string): string { let output = __pulse_fastly_redact_fields(input); for (let i = 0; i < __pulse_fastly_redactions.length; i += 1) output = __pulse_fastly_replace_secret(output, unchecked(__pulse_fastly_redactions[i])); return output }
1726
- function host_log(level: i32, messageHandle: i32): void { let endpoint = __pulse_fastly_log_handle; if (endpoint == -2) return; if (endpoint == -1) { const name = __pulse_fastly_utf8("stdout"); const out = __pulse_fastly_out_i32(); if (fastly_log_endpoint_get(changetype<usize>(name), name.byteLength, changetype<usize>(out)) != FASTLY_STATUS_OK) { __pulse_fastly_log_handle = -2; return } endpoint = __pulse_fastly_out_value(out); __pulse_fastly_log_handle = endpoint } let label = "unknown"; if (level == 1) label = "error"; else if (level == 2) label = "warn"; else if (level == 3) label = "info"; else if (level == 4) label = "debug"; const bytes = __pulse_fastly_utf8("[pulse:" + label + "] " + __pulse_fastly_redact(__pulse_fastly_string(messageHandle)) + "\n"); const written = __pulse_fastly_out_i32(); fastly_log_write(endpoint, changetype<usize>(bytes), bytes.byteLength, changetype<usize>(written)) }
1731
+ function host_log(level: i32, messageHandle: i32): void { let label = "unknown"; if (level == 1) label = "error"; else if (level == 2) label = "warn"; else if (level == 3) label = "info"; else if (level == 4) label = "debug"; const bytes = __pulse_fastly_utf8("[pulse:" + label + "] " + __pulse_fastly_redact(__pulse_fastly_string(messageHandle)) + "\n"); __pulse_fastly_write_stdout(bytes) }
1727
1732
  function __pulse_fastly_number(handle: i32): f64 { const value = __pulse_fastly_value(handle); if (value.kind == PULSE_VALUE_NUMBER) return value.number; if (value.kind == PULSE_VALUE_BOOLEAN) return value.boolean != 0 ? 1.0 : 0.0; if (value.kind == PULSE_VALUE_STRING) return F64.parseFloat(value.text); return 0.0 }
1728
1733
  function __pulse_fastly_hex(value: i32): string { return String.fromCharCode(value < 10 ? 48 + value : 87 + value) }
1729
1734
  function __pulse_fastly_is_unpaired(value: string, i: i32): bool {
@@ -1800,7 +1805,7 @@ function __pulse_fastly_fetch_body(value: __PulseFastlyValue): string { if (valu
1800
1805
  function __pulse_fastly_scalar(kind: i32, boolean: i32, slot: i32): i32 {
1801
1806
  const cached = unchecked(__pulse_fastly_scalar_handles[slot]); if (cached != 0) return cached;
1802
1807
  const item = new __PulseFastlyValue(); item.kind = kind; item.boolean = boolean;
1803
- const handle = __pulse_fastly_put(item); unchecked(__pulse_fastly_scalar_handles[slot] = handle); return handle
1808
+ const handle = __pulse_fastly_put(item); unchecked(__pulse_fastly_scalar_handles[slot] = handle); ${options.handleTracing ? 'if (handle != 0) __pulse_handle_cache_roots.push(handle);' : ''} return handle
1804
1809
  }
1805
1810
  function host_value_undefined(): i32 { return __pulse_fastly_scalar(PULSE_VALUE_UNDEFINED, 0, 0) }
1806
1811
  function host_value_null(): i32 { return __pulse_fastly_scalar(PULSE_VALUE_NULL, 0, 1) }
@@ -1811,13 +1816,13 @@ function host_value_number(value: f64): i32 {
1811
1816
  const index = cacheable ? i32(value) : -1;
1812
1817
  if (index >= 0) { const cached = unchecked(__pulse_fastly_integer_handles[index]); if (cached != 0) return cached }
1813
1818
  const item = new __PulseFastlyValue(); item.kind = PULSE_VALUE_NUMBER; item.number = value;
1814
- const handle = __pulse_fastly_put(item); if (index >= 0) unchecked(__pulse_fastly_integer_handles[index] = handle); return handle
1819
+ const handle = __pulse_fastly_put(item); if (index >= 0) { unchecked(__pulse_fastly_integer_handles[index] = handle); ${options.handleTracing ? 'if (handle != 0) __pulse_handle_cache_roots.push(handle);' : ''} } return handle
1815
1820
  }
1816
1821
  function host_value_string(pointer: i32, length: i32): i32 {
1817
1822
  const text = changetype<string>(pointer);
1818
1823
  if (text.length > 1 && text.length <= 128 && __pulse_fastly_literal_handles.has(text)) return __pulse_fastly_literal_handles.get(text);
1819
1824
  const handle = __pulse_fastly_string_value(text);
1820
- if (text.length > 1 && text.length <= 128 && __pulse_fastly_literal_handles.size < 1024) __pulse_fastly_literal_handles.set(text, handle);
1825
+ if (text.length > 1 && text.length <= 128 && __pulse_fastly_literal_handles.size < 1024) { __pulse_fastly_literal_handles.set(text, handle); ${options.handleTracing ? 'if (handle != 0) __pulse_handle_cache_roots.push(handle);' : ''} }
1821
1826
  return handle
1822
1827
  }
1823
1828
  function host_value_array(): i32 { const item = new __PulseFastlyValue(); item.kind = PULSE_VALUE_ARRAY; return __pulse_fastly_put(item) }
@@ -2055,7 +2060,16 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
2055
2060
  );
2056
2061
  }
2057
2062
  const bindings = resolveCapabilityBindings(plan, options);
2058
- const portable = packageApplication.enabled(plan) ? packageApplication.portable(plan) : generateCanonicalNativeAssemblyScript(plan, options);
2063
+ // Keep final provider eligibility authoritative over the portable artifact
2064
+ // and caller options. Only the selected crypto guest uses the linked policy;
2065
+ // that policy excludes active codecs regardless of JSON arena configuration.
2066
+ const numericRegions = facts.guestUnits.length === 0 ? 'fastly-numeric-regions-v2'
2067
+ : jwtCrypto && jwtCrypto.guestUnitRequired ? 'fastly-linked-numeric-regions-v1' : undefined;
2068
+ const portable = packageApplication.enabled(plan) ? packageApplication.portable(plan) : generateCanonicalNativeAssemblyScript(plan, {
2069
+ ...options, numericRegions,
2070
+ handleOwnership: facts.guestUnits.length === 0 ? 'fastly-handle-tracing-v1' : undefined
2071
+ });
2072
+ nativeHandleOwnership.validateRoots(plan, portable.manifest);
2059
2073
  const portableSource = stripPulseHostImports(portable.source);
2060
2074
  const hasJwt = (plan.effects || []).some((effect) => effect.kind === 'jwt.verify');
2061
2075
  const hasSign = plan.effects.some(effect => effect.kind === 'jwt.sign');
@@ -2066,6 +2080,7 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
2066
2080
  '',
2067
2081
  fastlyRuntimeSource(plan, bindings, {
2068
2082
  ...options,
2083
+ handleTracing: portable.manifest.handleOwnership?.enabled === true,
2069
2084
  jwtCrypto,
2070
2085
  es256KeyRecords
2071
2086
  }),
@@ -2082,7 +2097,10 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
2082
2097
  require('./native-fetch-deadlines.js').runtimeSource(plan),
2083
2098
  applicationErrors.settlementEnabled(plan) ? applicationErrors.runtimeSource(plan) : '',
2084
2099
  effectInvocations.runtimeSource({ packageCompletion: packageApplication.enabled(plan) }),
2100
+ ...(portable.manifest.numericRegions?.accounting === 'fastly-read-loop-v1' && portable.manifest.numericRegions.regions.length > 0
2101
+ ? [nativeValueBudget.numericRegionSource()] : []),
2085
2102
  portableSource,
2103
+ nativeHandleOwnership.providerSource(plan, portable.manifest.handleOwnership?.enabled === true),
2086
2104
  driverSource(plan, { guestLinked: facts.guestUnits.length > 0 }),
2087
2105
  ''
2088
2106
  ].join('\n');
@@ -2096,6 +2114,7 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
2096
2114
  source = effectInvocations.instrument(source);
2097
2115
  source = require('./request-budget.js').instrumentRequestBudget(source, bindings.maxDurationMs, plan);
2098
2116
  source = nativeValueBudget.instrument(source, plan);
2117
+ source = require('./native-body-forwarding.js').instrument(source, plan, bindings);
2099
2118
  const effectKinds = Object.freeze((plan.effects || []).reduce((output, effect) => {
2100
2119
  output[effect.kind] = (output[effect.kind] || 0) + 1;
2101
2120
  return output;
@@ -2119,6 +2138,14 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
2119
2138
  portableGeneratorVersion: portable.version,
2120
2139
  portableSourceHash: portable.sourceHash,
2121
2140
  schemaCodecs: portable.manifest.schemaCodecs,
2141
+ ...(portable.manifest.numericRegions ? { numericRegions: portable.manifest.numericRegions } : {}),
2142
+ handleOwnership: portable.manifest.handleOwnership ? Object.freeze({ ...portable.manifest.handleOwnership,
2143
+ providerRoots: portable.manifest.handleOwnership.enabled ? Object.freeze(nativeHandleOwnership.providerRoots(plan)) : Object.freeze([])
2144
+ }) : Object.freeze({
2145
+ policy: null, enabled: false,
2146
+ reason: packageApplication.enabled(plan) ? 'package-owned-emitter' : 'linked-runtime',
2147
+ roots: Object.freeze([]), checkpoints: 'none'
2148
+ }),
2122
2149
  handlerBodies: portable.manifest.handlerBodies,
2123
2150
  ...(portable.manifest.reportOwnership ? { reportOwnership: portable.manifest.reportOwnership } : {}),
2124
2151
  dispatcher: portable.manifest.dispatcher,
@@ -2171,7 +2198,7 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
2171
2198
  providerNeutralInput: true,
2172
2199
  javascriptRuntime: false,
2173
2200
  jsComputeRuntime: false,
2174
- wasi: plan.effects.some(effect => effect.kind === 'fetch') || bindings.maxDurationMs !== undefined || hasJwt || hasSign || bindings.s3.length || plan.effects.some(effect => conditionalKv.KV_CONDITIONAL_KINDS.includes(effect.kind)) ? 'clock_time_get only' : false,
2201
+ wasi: requiredImportsForPlan(plan, bindings).filter(key => key.startsWith('wasi_snapshot_preview1:')).map(key => key.split(':')[1]).join(', ').replace(/(.+)/, '$1 only') || false,
2175
2202
  effects: FASTLY_NATIVE_PLATFORM_EFFECT_KINDS.join(', '),
2176
2203
  continuations: true,
2177
2204
  groupedEffects: 'existing fetch start-before-wait ordering remains intact',
@@ -2183,7 +2210,7 @@ function generateFastlyNativePlatformCapabilitiesAssemblyScript(plan, options =
2183
2210
  kv: 'Fastly KV Store async lookup/insert; conditional operations preserve u64 generations, bounded readiness, strict Pulse envelopes and uncertain dispatch',
2184
2211
  assets: 'Fastly KV Store lookup with provider-owned content type, cache headers, and GET/HEAD body ownership',
2185
2212
  grip: 'canonical stateless framing plus bound, secret-referenced broadcast HTTP realization and stable acknowledgement',
2186
- logging: 'best-effort redacted writes to the provider-owned stdout logging endpoint',
2213
+ logging: 'best-effort redacted WASI fd_write to stdout (descriptor 1), one attempt per statement',
2187
2214
  schemas: 'fetch JSON decode and structured response encode',
2188
2215
  opaqueResponse: 'origin fetch response/body handles and direct GRIP hold responses pass through without JavaScript runtime ownership',
2189
2216
  transportErrors: 'host status failures remain distinct from HTTP response status',
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // Shared by both Native emitters. Fastly's named logging endpoint ABI rejects
4
+ // stdout/stderr; Compute stdio uses WASI fd_write with stdout descriptor 1.
5
+ const nativeStdioSource = String.raw`
6
+ @external("wasi_snapshot_preview1", "fd_write")
7
+ declare function __pulse_fastly_fd_write(fd: i32, iovs: usize, count: i32, written: usize): i32
8
+
9
+ function __pulse_fastly_write_stdout(bytes: ArrayBuffer): void {
10
+ const iov = new StaticArray<usize>(2)
11
+ unchecked(iov[0] = changetype<usize>(bytes))
12
+ unchecked(iov[1] = bytes.byteLength)
13
+ const written = new StaticArray<i32>(1)
14
+ // One best-effort attempt: errors, zero progress, and short writes never
15
+ // retry, spin, change the response, or disable subsequent log statements.
16
+ __pulse_fastly_fd_write(1, changetype<usize>(iov), 1, changetype<usize>(written))
17
+ }
18
+ `;
19
+
20
+ module.exports = { nativeStdioSource };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- // Host value handles live for the invocation. Store concatenation as shared,
4
- // immutable prefixes instead of retaining a flat copy for every intermediate.
3
+ // Store concatenation as shared, immutable prefixes instead of retaining a flat
4
+ // copy for every intermediate. Direct object edges preserve live rope children.
5
5
  // This changes representation only: coercion happens at the original operation,
6
6
  // and aliases keep their original string even after a later append.
7
7
  const nativeStringFields = `
@@ -35,6 +35,38 @@ export function pulse_fastly_memory_values(): i64 { return __pulse_memory_values
35
35
  `;
36
36
  }
37
37
 
38
+ // These helpers are private to proven regions in an unlinked read-loop plan.
39
+ // Cache admissions use the original operations, so later boxed code sees the
40
+ // same cache and charges. Uncached numbers pay at each original evaluation;
41
+ // only their physical materialization is delayed until a region exit.
42
+ function numericRegionSource() {
43
+ return `
44
+ function __pulse_numeric_cacheable(value: f64): bool {
45
+ return value >= 0 && value < 65536 && Math.floor(value) == value && (value != 0 || 1.0 / value > 0)
46
+ }
47
+ function __pulse_numeric_charge_number(value: f64): f64 {
48
+ __pulse_memory_charge(0, 0)
49
+ if (__pulse_numeric_cacheable(value)) host_value_number(value)
50
+ else __pulse_memory_charge(1, ${policy.valueBytes})
51
+ return value
52
+ }
53
+ function __pulse_numeric_charge_boolean(value: bool): bool {
54
+ __pulse_memory_charge(0, 0)
55
+ host_value_boolean(value ? 1 : 0)
56
+ return value
57
+ }
58
+ function __pulse_numeric_truthy(value: f64): bool { return value != 0.0 && !isNaN(value) }
59
+ function __pulse_numeric_box(value: f64): i32 {
60
+ __pulse_memory_charge(0, 0)
61
+ if (__pulse_numeric_cacheable(value)) return host_value_number(value)
62
+ const item = new __PulseFastlyValue(); item.kind = PULSE_VALUE_NUMBER; item.number = value
63
+ // Already charged at the original expression, never a general put bypass.
64
+ __pulse_fastly_values.push(item)
65
+ return __pulse_fastly_values.length
66
+ }
67
+ `;
68
+ }
69
+
38
70
  function instrument(source, plan) {
39
71
  if (!hasBoundedReadLoop(plan)) return source;
40
72
  function insert(needle, extra) {
@@ -55,4 +87,4 @@ function instrument(source, plan) {
55
87
  return source + runtimeSource();
56
88
  }
57
89
 
58
- module.exports = { policy, hasBoundedReadLoop, runtimeSource, instrument };
90
+ module.exports = { policy, hasBoundedReadLoop, runtimeSource, numericRegionSource, instrument };
@@ -10,8 +10,8 @@ class __PulseFastlyAdmissionFrame {
10
10
  }
11
11
  function __pulse_fastly_admission_enter(handle: i32, budget: __PulseJsonAdmissionBudget,
12
12
  stack: Array<__PulseFastlyAdmissionFrame>, active: Set<i32>): bool {
13
- if (handle <= 0 || handle > __pulse_fastly_values.length) return budget.fail(12)
14
- const value = __pulse_fastly_values[handle - 1]
13
+ if (!__pulse_fastly_has_value(handle)) return budget.fail(12)
14
+ const value = __pulse_fastly_value(handle)
15
15
  const array = value.kind == PULSE_VALUE_ARRAY
16
16
  const object = value.kind == PULSE_VALUE_OBJECT
17
17
  if (!budget.node(stack.length + (array || object ? 1 : 0))) return false
@@ -36,7 +36,7 @@ function __pulse_fastly_admit_bounded_value(handle: i32, limits: __PulseJsonAdmi
36
36
  if (!__pulse_fastly_admission_enter(handle, budget, stack, active)) return budget
37
37
  while (stack.length > 0) {
38
38
  const frame = stack[stack.length - 1]
39
- const value = __pulse_fastly_values[frame.handle - 1]
39
+ const value = __pulse_fastly_value(frame.handle)
40
40
  if (frame.index == value.values.length) { active.delete(frame.handle); stack.pop(); continue }
41
41
  const index = frame.index++
42
42
  if (index > 0 && !budget.addBytes(1)) return budget
package/src/config-api.js CHANGED
@@ -39,6 +39,7 @@ function fastly(options = {}) {
39
39
  const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
40
40
  return Object.freeze({
41
41
  kind: 'fastly',
42
+ ...(options.bodyForwarding === undefined ? {} : {bodyForwarding: require('./toolchain/body-forwarding.js').normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs)}),
42
43
  ...(options.maxDurationMs === undefined ? {} : { maxDurationMs: require('@pulse-compute/runtime/host').normalizeRequestDuration(options.maxDurationMs) }),
43
44
  version: FASTLY_PROVIDER_API_VERSION,
44
45
  bindings: Object.freeze({
@@ -82,6 +82,19 @@
82
82
  },
83
83
  "fastly.maxWasmBytes": {
84
84
  "kind": "positive-safe-integer"
85
+ },
86
+ "fastly.bodyForwarding": {
87
+ "kind": "object"
88
+ },
89
+ "fastly.bodyForwarding.requestMaxBytes": {
90
+ "kind": "integer",
91
+ "minimum": 1,
92
+ "maximum": 1048576
93
+ },
94
+ "fastly.bodyForwarding.responseMaxBytes": {
95
+ "kind": "integer",
96
+ "minimum": 1,
97
+ "maximum": 1048576
85
98
  }
86
99
  }
87
100
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ const { normalizeBodyForwarding } = require('../src/toolchain/body-forwarding.js') as { normalizeBodyForwarding(value: PulseFastlyBodyForwardingOptions | undefined, maxDurationMs?: number): PulseFastlyBodyForwardingOptions | undefined };
1
2
  const { normalizeFastlyMaxWasmBytes } = require('../src/build/wasm-budget.js') as { normalizeFastlyMaxWasmBytes(value?: number): number };
2
3
  const { normalizeRequestDuration } = require('@pulse-compute/runtime/host') as { normalizeRequestDuration(value?: number): number | undefined };
3
4
  import FASTLY_CONFIG_SCHEMA from './config-schema.json';
@@ -24,10 +25,17 @@ export interface PulseFastlyLocalOptions {
24
25
  readonly networkFetch?: boolean;
25
26
  }
26
27
 
28
+ export interface PulseFastlyBodyForwardingOptions {
29
+ readonly requestMaxBytes: number;
30
+ readonly responseMaxBytes: number;
31
+ }
32
+
27
33
  export interface PulseFastlyProviderOptions {
28
34
  /** Inclusive final Native Wasm byte budget; default 4 MiB. */
29
35
  readonly maxWasmBytes?: number;
30
36
  readonly maxDurationMs?: number;
37
+ /** Experimental Native metered pump; final close hands delivery to Fastly. */
38
+ readonly bodyForwarding?: PulseFastlyBodyForwardingOptions;
31
39
  readonly configStore?: string;
32
40
  readonly secretStore?: string;
33
41
  readonly kv?: Readonly<Record<string, string>>;
@@ -42,6 +50,8 @@ export interface PulseFastlyProviderOptions {
42
50
 
43
51
  export interface PulseFastlyProviderConfig {
44
52
  readonly maxDurationMs?: number;
53
+ /** Experimental Native metered pump; final close hands delivery to Fastly. */
54
+ readonly bodyForwarding?: PulseFastlyBodyForwardingOptions;
45
55
  readonly kind: 'fastly';
46
56
  readonly version: typeof FASTLY_PROVIDER_API_VERSION;
47
57
  readonly bindings: {
@@ -105,6 +115,7 @@ export function fastly(options: PulseFastlyProviderOptions = {}): PulseFastlyPro
105
115
  return Object.freeze({
106
116
  kind: 'fastly',
107
117
  maxDurationMs: normalizeRequestDuration(options.maxDurationMs),
118
+ bodyForwarding: normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs),
108
119
  version: FASTLY_PROVIDER_API_VERSION,
109
120
  bindings: Object.freeze({
110
121
  configStore: String(options.configStore ?? fastlyDefaults['fastly.configStore']),
@@ -12,13 +12,14 @@ const FASTLY_PROVIDER_DESCRIPTOR = canonical.normalizeDescriptor({
12
12
  deployable: true,
13
13
  localExecution: true,
14
14
  capabilities: [
15
- 'request', 'response.json', 'response.text', 'response.custom', 'fetch',
15
+ 'request', 'request.body.forward', 'response.json', 'response.text', 'response.custom', 'fetch',
16
16
  'time.wall-clock', 'crypto.digestText', 'config.get', 'secret.get', 'kv.get', 'kv.put', 'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'assets.lookup',
17
17
  'grip.channel', 'grip.hold', 'grip.publish', 'grip.broadcast', 'jwt.verify', 'jwt.sign',
18
18
  'opaque.pass-through', 's3.head', 's3.getText', 's3.putText', 's3.getBody'
19
19
  ],
20
20
  lowering: {
21
21
  request: 'fastly.compute.request',
22
+ 'request.body.forward': 'fastly.native.body-forwarding',
22
23
  'response.json': 'fastly.compute.response.json',
23
24
  'response.text': 'fastly.compute.response.text',
24
25
  'response.custom': 'fastly.compute.response.custom',
@@ -62,10 +63,12 @@ function validateStaticBackendBindings(metadata = {}, bindings = {}) {
62
63
  }
63
64
  }
64
65
 
65
- function createFastlyLoweringPlan(metadata, bindings = {}) {
66
+ function createFastlyLoweringPlan(metadata, bindings = {}, options = {}) {
67
+ const forwarding = require('./toolchain/body-forwarding.js').normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs);
68
+ const descriptor = forwarding ? FASTLY_PROVIDER_DESCRIPTOR : {...FASTLY_PROVIDER_DESCRIPTOR, capabilities: FASTLY_PROVIDER_DESCRIPTOR.capabilities.filter(id => id !== 'request.body.forward')};
66
69
  require('./toolchain/s3.js').resolveFastlyS3(metadata.providerOperations || [], bindings.s3);
67
70
  validateStaticBackendBindings(metadata, bindings);
68
- return canonical.createProviderLoweringPlan(metadata, FASTLY_PROVIDER_DESCRIPTOR, {
71
+ return canonical.createProviderLoweringPlan(metadata, descriptor, {
69
72
  ...bindings,
70
73
  grip: bindings.grip || 'fastly-fanout'
71
74
  });