@pulse-compute/provider-fastly 1.0.0-beta.10 → 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 (36) hide show
  1. package/README.md +6 -6
  2. package/package.json +11 -11
  3. package/src/build/canonical-target.js +6 -0
  4. package/src/build/effect-invocations.js +6 -4
  5. package/src/build/host-extensions.js +254 -0
  6. package/src/build/lifecycle.js +50 -0
  7. package/src/build/native-body-forwarding.as.ts +28 -44
  8. package/src/build/native-body-forwarding.js +15 -11
  9. package/src/build/native-fetch-accounting.as.ts +133 -0
  10. package/src/build/native-fetch-accounting.js +84 -0
  11. package/src/build/native-fetch-bounds.js +47 -0
  12. package/src/build/native-fetch-cohorts.as.ts +126 -0
  13. package/src/build/native-fetch-cohorts.js +65 -0
  14. package/src/build/native-fetch-deadlines.as.ts +13 -8
  15. package/src/build/native-fetch-deadlines.js +13 -4
  16. package/src/build/native-fetch-errors.js +2 -2
  17. package/src/build/native-fetch-headers.js +52 -0
  18. package/src/build/native-fetch-outcomes.js +87 -0
  19. package/src/build/native-fetch-timing.as.ts +74 -0
  20. package/src/build/native-fetch-timing.js +50 -0
  21. package/src/build/native-forwarding-accounting.as.ts +153 -0
  22. package/src/build/native-forwarding-accounting.js +47 -0
  23. package/src/build/native-header-lookup.js +2 -2
  24. package/src/build/native-http-effects.js +10 -4
  25. package/src/build/native-platform-capabilities.js +89 -39
  26. package/src/build/native-request-body.js +6 -5
  27. package/src/build/{request-budget.as.ts → request-budget-source.js} +12 -4
  28. package/src/build/request-budget.js +7 -8
  29. package/src/build/response-authority.js +56 -0
  30. package/src/javascript/fetch-adapter.js +3 -0
  31. package/src/javascript/runtime-host.js +1 -0
  32. package/src/provider-contract.js +2 -1
  33. package/src/testing/native-http-effects-host.js +9 -0
  34. package/src/testing/native-platform-capabilities-host.js +10 -0
  35. package/src/toolchain/index.js +2 -1
  36. package/src/toolchain/native-execution.js +7 -2
@@ -0,0 +1,133 @@
1
+ // Fixed one-cohort adapter. The existing effect owner and scheduler retain all
2
+ // dispatch/completion authority. Claims and retained text are never recycled.
3
+ let __fetch_ra: PulseResourceLedger | null = null;
4
+ let __fca_origin:i64 = 0;
5
+ let __fca_clock_failed:bool = false;
6
+ let __fca_reaped:bool = false;
7
+ const __fca_io:usize = memory.data(32);
8
+ const __fca_exit:usize = memory.data(64);
9
+ function __fca_ns(slot:i32):u32 { return slot == 0 ? 1 : slot == 2 ? 4 : slot == 3 || slot == 5 ? 2 : 3; }
10
+ function __fca_now():u64 {
11
+ if (__fca_clock_failed) return PRL_SAFE + 1;
12
+ const now = __request_clock();
13
+ if (now < __fca_origin || __request_expired) { __fca_clock_failed = true; return PRL_SAFE + 1; }
14
+ if (now >= __request_deadline) __request_expired = true;
15
+ return u64(now - __fca_origin);
16
+ }
17
+ function __fca_work():bool { return __fca_ok(__fca_now() < __FCA_DURATION); }
18
+ function __fca_ok(ok:bool):bool {
19
+ if (!ok) __pulse_fastly_fail(PULSE_ERROR_HOSTCALL,272,0);
20
+ return ok;
21
+ }
22
+ function __fca_start():bool {
23
+ if (__fetch_ra !== null || __request_deadline < 0 || __request_last_clock < 0
24
+ || __pulse_invocation_count != 1 || __pulse_invocation_tickets[0] != 1) return __fca_ok(false);
25
+ __fca_origin = __request_deadline - i64(__FCA_DURATION);
26
+ const l = new PulseResourceLedger(1,__FCA_RECORDS,7,__FCA_BACKING,7,1,__FCA_DURATION,__FCA_STORAGE);
27
+ __fetch_ra = l;
28
+ if (!l.begin(1,__fca_now(),5,2,1,true)) return __fca_ok(false);
29
+ for (let i=0;i<7;i++) if (!l.stageRecord(1,1,__fca_ns(i),1,i>=5?3:1,0)) return __fca_ok(false);
30
+ if (!l.stageRecord(1,2,0,1,1,100) || !l.stageRecord(1,2,0,1,1,__FCA_ENCODING)
31
+ || !l.stageRecord(1,2,0,1,1,__FCA_READER) || !l.stageRecord(1,2,0,1,3,64)
32
+ || !l.stageRecord(1,3,0,1,1,0)
33
+ /*FCA_HEADER_STAGE*/ || !l.commit(1,__fca_now())) return __fca_ok(false);
34
+ for (let i=0;i<__FCA_RECORDS;i++) if (l.reservedSlot(i)!=i || l.reservedGeneration(i)!=1) return __fca_ok(false);
35
+ return __fca_ok(l.consume(1,7,1,1,100,false,__fca_now())
36
+ && l.consume(1,8,1,1,__FCA_ENCODING,false,__fca_now())
37
+ && l.consume(1,9,1,1,__FCA_READER,false,__fca_now())
38
+ /*FCA_HEADER_CONSUME*/);
39
+ }
40
+ function __fca_acquire(slot:i32):bool {
41
+ return __fetch_ra !== null && __fca_ok(__fetch_ra!.beginAcquire(1,slot,1,__fca_ns(slot),1,slot>=5,__fca_now()));
42
+ }
43
+ function __fca_acquired(slot:i32,status:i32,handle:i32):i32 {
44
+ const l=__fetch_ra!;
45
+ if(status==0) __fca_ok(l.acquired(1,slot,1,__fca_ns(slot),1,u32(handle)));
46
+ else l.noAcquisition(1,slot,1,__fca_ns(slot),1,false);
47
+ return status;
48
+ }
49
+ function __fca_new(slot:i32,out:usize):i32 {
50
+ if(!__fca_acquire(slot)) return 1;
51
+ const ns=__fca_ns(slot);
52
+ const status=ns==1?fastly_http_req_new(out):ns==2?fastly_http_resp_new(out):fastly_http_body_new(out);
53
+ return __fca_acquired(slot,status,load<i32>(out));
54
+ }
55
+ function __fca_dispose(slot:i32):i32 {
56
+ if(__fetch_ra===null) return 1;
57
+ const l=__fetch_ra!,ns=__fca_ns(slot);
58
+ if(l.bank[slot*8+5]!=PRL_LIVE || !l.beginDispose(1,slot,1,ns,1,__fca_now())) return 1;
59
+ const handle=i32(l.bank[slot*8+6]);
60
+ const status=ns==1?__pulse_fetch_close_request(handle):ns==2?__pulse_fetch_close_response(handle):__pulse_fetch_close_body(handle);
61
+ l.disposed(1,slot,1,ns,1,status==0); return status;
62
+ }
63
+ function __fca_dispatch(request:i32,body:i32,backend:usize,length:i32,out:usize):i32 {
64
+ if(!__fca_acquire(2)||!__fca_acquire(3)||!__fca_acquire(4)) return 1;
65
+ const l=__fetch_ra!;
66
+ if(!l.beginDispose(1,0,1,1,1,__fca_now())||!l.beginDispose(1,1,1,3,1,__fca_now())) return 1;
67
+ const status=fastly_http_req_send_async(request,body,backend,length,out);
68
+ l.disposed(1,0,1,1,1,status==0); l.disposed(1,1,1,3,1,status==0);
69
+ return __fca_acquired(2,status,load<i32>(out));
70
+ }
71
+ function __fca_poll(handle:i32,detail:usize,done:usize,response:usize,body:usize):i32 {
72
+ const l=__fetch_ra!;
73
+ if(l.bank[2*8+5]!=PRL_LIVE || __fca_now()>=__FCA_DURATION) return 1;
74
+ const status=__pulse_fetch_poll(handle,detail,done,response,body), ready=load<i32>(done);
75
+ if(status!=0 || (ready!=0 && ready!=1)) {
76
+ l.uncertain(1,2,1,4,1); l.noAcquisition(1,3,1,2,1,false); l.noAcquisition(1,4,1,3,1,false);
77
+ return status==0?2:status;
78
+ }
79
+ if(ready==1) {
80
+ l.transferred(1,2,1,4,1);
81
+ __fca_acquired(3,0,load<i32>(response)); __fca_acquired(4,0,load<i32>(body));
82
+ }
83
+ return 0;
84
+ }
85
+ function __fca_cleanup(final:bool=false):void {
86
+ if(__fetch_ra===null) return;
87
+ const l=__fetch_ra!;
88
+ l.close(1);
89
+ if(!__fca_reaped && l.bank[2*8+5]==PRL_LIVE) {
90
+ __fca_reaped=true;
91
+ if(l.cleanupStep(1,false,__fca_now())) {
92
+ __pulse_send_error_detail_reset(__fca_io); store<i32>(__fca_io+20,0);
93
+ __fca_poll(i32(l.bank[2*8+6]),__fca_io,__fca_io+20,__fca_io+24,__fca_io+28);
94
+ }
95
+ }
96
+ for(let i=0;i<7;i++) {
97
+ const state=l.bank[i*8+5];
98
+ if(state==PRL_RESERVED && (i<5||final)) l.unused(1,i,1,__fca_ns(i),1);
99
+ else if(state==PRL_LIVE && i!=2) __fca_dispose(i);
100
+ }
101
+ }
102
+ function __fca_error():bool {
103
+ if(__fetch_ra===null) return false;
104
+ const l=__fetch_ra!;
105
+ // Capture the primary driver failure before cleanup or protected delivery.
106
+ const status=__request_expired || __pulse_fastly_error_stage==271 ? 504
107
+ : __pulse_fastly_last_error==PULSE_ERROR_TRANSPORT ? 502 : 500;
108
+ if(l.primary==0) l.fail(1,u32(status));
109
+ __fca_cleanup();
110
+ if(!__pulse_response_owned() || !l.consume(1,10,1,1,64,true,__fca_now())) return true;
111
+ if(__fca_new(5,__fca_io+24)!=0) return true;
112
+ const response=load<i32>(__fca_io+24);
113
+ if(__fca_new(6,__fca_io+28)!=0) return true;
114
+ const body=load<i32>(__fca_io+28);
115
+ if(__fca_now()>=__FCA_DURATION || fastly_http_resp_status_set(response,status)!=0) return true;
116
+ const header='content-typetext/plain; charset=utf-8';
117
+ for(let i=0;i<header.length;i++) store<u8>(__fca_exit+i,u8(header.charCodeAt(i)));
118
+ if(__fca_now()>=__FCA_DURATION || fastly_http_resp_header_append(response,__fca_exit,12,__fca_exit+12,header.length-12)!=0) return true;
119
+ const text=status==504?'Gateway Timeout':status==502?'Bad Gateway':'Internal Server Error';
120
+ for(let i=0;i<text.length;i++) store<u8>(__fca_exit+i,u8(text.charCodeAt(i)));
121
+ let offset=0;
122
+ while(offset<text.length) {
123
+ if(__fca_now()>=__FCA_DURATION) return true;
124
+ const result=fastly_http_body_write(body,__fca_exit+offset,text.length-offset,0,__fca_io+20);
125
+ const written=load<i32>(__fca_io+20);
126
+ if(result!=0 || written<=0 || written>text.length-offset) return true;
127
+ offset+=written;
128
+ }
129
+ if(!l.beginDispose(1,5,1,2,1,__fca_now()) || !l.beginDispose(1,6,1,3,1,__fca_now())) return true;
130
+ const sent=fastly_http_resp_send_downstream(response,body,0);
131
+ l.disposed(1,5,1,2,1,sent==0); l.disposed(1,6,1,3,1,sent==0);
132
+ return true;
133
+ }
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const {resourceAdmission: admission} = require('@pulse-compute/wasm-contracts/provider/toolchain');
5
+ const {resourceLedgerSource} = require('@pulse-compute/wasm-runtime-core-as/compiler/canonical-native');
6
+ const fixed = (id, bytes) => ({id, kind: 'fixed', bytes, copies: 1});
7
+ function profile(plan, bindings, headerBufferBytes) {
8
+ // Prove one visit and one decode from the canonical control tree, not merely
9
+ // the number of static effect sites. No authored accounting configuration.
10
+ const body = plan.entry?.body, effect = plan.effects[0];
11
+ const decoder = effect?.decoder;
12
+ if (plan.entry?.kind !== 'handler' || plan.handlers.length || plan.routing || plan.packages?.application
13
+ || plan.effects.length !== 1 || effect.kind !== 'fetch' || effect.grouped || !['text','json'].includes(decoder)
14
+ || body?.length !== 2 || body[0].kind !== 'effect' || body[0].effectId !== effect.id
15
+ || body[1].kind !== 'return' || body[1].value?.name !== 'response.' + decoder
16
+ || body[1].value.arguments.length !== 1 || body[1].value.arguments[0].kind !== 'local'
17
+ || body[1].value.arguments[0].id !== effect.result?.localId) return null;
18
+ const init = effect.inputs.find(x => x.name === 'init')?.value;
19
+ const url = effect.inputs.find(x => x.name === 'url')?.value;
20
+ const uniqueKeys = object => object?.kind === 'object'
21
+ && object.entries.every(x => x.kind === 'property' && x.key?.kind === 'literal' && typeof x.key.value === 'string')
22
+ && new Set(object.entries.map(x => x.key.value)).size === object.entries.length;
23
+ const field = name => init?.entries?.find(x => x.key?.value === name)?.value;
24
+ const timeout = field('timeoutMs'), headers = field('headers');
25
+ const backend = bindings.backends.find(x => x.index === 0)?.backend;
26
+ const bound = plan.schemas?.registry?.maxBytes;
27
+ if (url?.kind !== 'literal' || typeof url.value !== 'string' || !uniqueKeys(init)
28
+ || init.entries.some(x => !['timeoutMs','headers'].includes(x.key?.value))
29
+ || timeout?.kind !== 'literal' || !Number.isSafeInteger(timeout.value) || timeout.value <= 0
30
+ || (headers && (!uniqueKeys(headers) || headers.entries.some(x => x.key?.kind !== 'literal'
31
+ || typeof x.key.value !== 'string' || x.value?.kind !== 'literal' || typeof x.value.value !== 'string')))
32
+ || typeof backend !== 'string' || !backend || bindings.dynamicBackends
33
+ || !Number.isInteger(bindings.maxDurationMs) || bindings.maxDurationMs < 1 || bindings.maxDurationMs > 30000
34
+ || !Number.isInteger(bound) || bound <= 0 || bound > 1073741823) return null;
35
+ const realization = `fastly.native.fetch-${decoder}.single-v1`;
36
+ const requireJson = decoder === 'json' && plan.schemas.registry.contentTypePolicy === 'require-json';
37
+ // The header reader owns C bytes, key encoding and ABI outputs. AS 0.28.18
38
+ // decodeUnsafe allocates 2N bytes; toLowerCase allocates 4N before shrinking.
39
+ // The first value has N <= C-1 bytes because the list is NUL-terminated.
40
+ if (requireJson && (!Number.isInteger(headerBufferBytes) || headerBufferBytes < 1
41
+ || headerBufferBytes > 2147483647)) throw new TypeError('Unproved JSON header backing');
42
+ const headerBytes = requireJson ? 7 * headerBufferBytes + 18 : 0;
43
+ const encodingBytes = 3 + Buffer.byteLength(url.value) + Buffer.byteLength(backend)
44
+ + (headers?.entries || []).reduce((n,h) => n + Buffer.byteLength(h.key.value) + Buffer.byteLength(h.value.value), 0);
45
+ // 12 request outputs + 40 scheduler outputs + 8 clock + 8 reused body-wait
46
+ // outputs + 32 cleanup/exit outputs. Reader payload backing is 3B+5.
47
+ const scratchBytes = 100, readerBytes = 3 * bound + 5;
48
+ const work = {handles:[{namespace:'http.request',count:1},{namespace:'http.body',count:2},
49
+ {namespace:'http.pending',count:1},{namespace:'http.response',count:1}],
50
+ allocations:[fixed('scratch',scratchBytes),fixed('encoding',encodingBytes),fixed('reader',readerBytes),
51
+ ...(requireJson ? [fixed('json-content-type',headerBytes)] : [])],records:1,cleanupSteps:0};
52
+ const cleanup = {handles:[],allocations:[],records:0,cleanupSteps:5};
53
+ const exit = {handles:[{namespace:'http.response',count:1},{namespace:'http.body',count:1}],
54
+ allocations:[fixed('error-text',64)],records:0,cleanupSteps:2};
55
+ const required = {handles:7,backingBytes:scratchBytes+encodingBytes+readerBytes+headerBytes+64,records:12+(requireJson?1:0),cleanupSteps:7,effects:1};
56
+ const result = admission.createPlan({version:admission.VERSION,realization,
57
+ selected:[{site:effect.id,count:1,profile:{version:admission.PROFILE_VERSION,owner:'provider-fastly',
58
+ operation:'single-fetch-'+decoder,realization,effects:1,work,cleanup,
59
+ exclusions:['incoming-request-context','application-values','diagnostic-storage','driver-identity-storage','successful-downstream-response','allocator-metadata',
60
+ ...(decoder === 'json' ? ['json-parser-and-schema-materialization'] : [])]}}],
61
+ exit,occupied:Object.fromEntries(admission.DIMENSIONS.map(k=>[k,0])),limits:required});
62
+ if (!result.admitted || Object.keys(required).some(k => result.required[k] !== required[k])) throw new Error('Fetch resource inventory drift');
63
+ return Object.freeze({...result,encodingBytes,readerBytes,scratchBytes,headerBytes,storageBytes:1024+128*required.records,
64
+ ...(requireJson ? {headerAdmission:{bufferBytes:headerBufferBytes,backingBytes:headerBytes}} : {}),
65
+ durationNs:bindings.maxDurationMs*1000000,selection:'canonical-single-get-'+decoder,
66
+ executionIdentity:'private-single-invocation-wasm-instance',
67
+ unprofiled:'other-fetch-shapes-repetition-groups-dynamic-inputs-and-javascript'});
68
+ }
69
+ function hooks(selected) {
70
+ if (!selected) return [];
71
+ const owner = 'fetch-accounting', order = 20;
72
+ return [
73
+ {point:'response.error',owner,order,body:' return __fca_error()'},
74
+ {point:'invocation.end',owner,order,body:' __fca_cleanup(true)'}
75
+ ];
76
+ }
77
+ function source(selected) {
78
+ if (!selected) return '';
79
+ return resourceLedgerSource(true) + `\nconst __FCA_RECORDS:i32=${selected.required.records};\nconst __FCA_STORAGE:u32=${selected.storageBytes};\nconst __FCA_HEADER:u64=${selected.headerBytes};\nconst __FCA_BACKING:u64=${selected.required.backingBytes};\nconst __FCA_ENCODING:u64=${selected.encodingBytes};\nconst __FCA_READER:u64=${selected.readerBytes};\nconst __FCA_DURATION:u64=${selected.durationNs};\n`
80
+ + fs.readFileSync(path.join(__dirname,'native-fetch-accounting.as.ts'),'utf8')
81
+ .replace('/*FCA_HEADER_STAGE*/', selected.headerBytes ? '|| !l.stageRecord(1,2,0,1,1,__FCA_HEADER)' : '')
82
+ .replace('/*FCA_HEADER_CONSUME*/', selected.headerBytes ? '&& l.consume(1,12,1,1,__FCA_HEADER,false,__fca_now())' : '');
83
+ }
84
+ module.exports = {profile,hooks,source};
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+ // Provider-local proof over canonical values. Unknown is not a zero allowance.
3
+ // Input materialization remains with the existing value/schema owners; this
4
+ // proves the strings and serializer copies consumed by the fetch service.
5
+ function create(plan,C,B){
6
+ const locals=new Map(),effects=new Map();let mutable=false;
7
+ const walk=(v,fn)=>{if(!v||typeof v!=='object')return;if(Array.isArray(v)){v.forEach(x=>walk(x,fn));return;}fn(v);Object.values(v).forEach(x=>walk(x,fn));};
8
+ walk(plan.entry,scan);walk(plan.handlers,scan);walk(plan.helpers,scan);walk(plan.stages,scan);
9
+ function scan(v){if(v.kind==='local'&&v.declaration==='const'&&v.value)locals.set(v.localId,v.value);if(/assign|update/.test(v.kind||'')||v.kind==='method-call'&&['push','pop','shift','unshift','splice'].includes(v.method))mutable=true;}
10
+ for(const e of plan.effects)if(e.kind==='fetch'&&e.result?.localId&&e.result.valueKind!=='fetch-response'&&e.decoder==='text')effects.set(e.result.localId,{chars:B,bytes:3*B});
11
+ function string(v,seen=new Set()){
12
+ if(!v)return null;
13
+ if(v.kind==='literal'&&typeof v.value==='string')return{chars:v.value.length,bytes:Buffer.byteLength(v.value)};
14
+ if(v.kind==='local'){
15
+ if(mutable)return null;
16
+ if(effects.has(v.id))return effects.get(v.id);
17
+ if(seen.has(v.id))return null;seen=new Set(seen);seen.add(v.id);return string(locals.get(v.id),seen);
18
+ }
19
+ if(v.kind==='context-read'&&v.path?.[0]==='req'&&['method','url','path'].includes(v.path[1]))return{chars:C,bytes:3*C};
20
+ if(v.kind==='intrinsic'&&v.name==='request.header')return{chars:C,bytes:3*C};
21
+ if(v.kind==='intrinsic'&&v.name==='request.text')return{chars:B,bytes:3*B};
22
+ if(v.kind==='method-call'&&v.receiver?.valueKind==='fetch-response'&&v.method==='text')return{chars:B,bytes:3*B};
23
+ if(v.kind==='binary'&&['+','||','??'].includes(v.operator)){
24
+ const a=string(v.left,seen),b=string(v.right,seen);if(!a||!b)return null;
25
+ return v.operator==='+'?{chars:a.chars+b.chars,bytes:a.bytes+b.bytes}:{chars:Math.max(a.chars,b.chars),bytes:Math.max(a.bytes,b.bytes)};
26
+ }
27
+ return null;
28
+ }
29
+ function json(v,seen=new Set()){
30
+ if(!v)return null;
31
+ if(v.kind==='local'){
32
+ if(mutable||seen.has(v.id))return null;seen=new Set(seen);seen.add(v.id);return json(locals.get(v.id),seen);
33
+ }
34
+ const s=string(v,seen);if(s)return{chars:6*s.chars+2,nodes:1,numbers:0};
35
+ if(v.kind==='literal'&&(v.value===null||typeof v.value==='boolean'))return{chars:5,nodes:1,numbers:0};
36
+ if(v.kind==='literal'&&typeof v.value==='number')return{chars:32,nodes:1,numbers:1};
37
+ let children;
38
+ if(v.kind==='array')children=v.items;
39
+ else if(v.kind==='object'&&v.entries.every(e=>e.kind==='property'&&e.key?.kind==='literal'&&typeof e.key.value==='string'))children=v.entries.flatMap(e=>[e.key,e.value]);
40
+ else return null;
41
+ if(!Array.isArray(children))return null;
42
+ const parts=children.map(x=>json(x,seen));if(parts.some(x=>!x))return null;
43
+ return parts.reduce((a,b)=>({chars:a.chars+b.chars+1,nodes:a.nodes+b.nodes,numbers:a.numbers+b.numbers}),{chars:2,nodes:1,numbers:0});
44
+ }
45
+ return{string,json};
46
+ }
47
+ module.exports={create};
@@ -0,0 +1,126 @@
1
+ // One immutable invocation partition per existing driver ticket. No recycling,
2
+ // second effect counter, or scheduling authority is introduced here.
3
+ let __fetch_ra:PulseResourceLedger|null=null;
4
+ let __fca_origin:i64=0;
5
+ let __fca_clock_failed:bool=false;
6
+ let __fca_frame:i32=0;
7
+ const __fca_reaped=new StaticArray<bool>(__FCA_COUNT);
8
+ const __fca_io:usize=memory.data(32);
9
+ const __fca_exit:usize=memory.data(64);
10
+ function __fca_slot(local:i32):i32{return local>=5?__FCA_HANDLES-2+local-5:__fca_frame*5+local;}
11
+ function __fca_ns(slot:i32):u32{return slot==__FCA_HANDLES-2?2:slot==__FCA_HANDLES-1?3:slot%5==0?1:slot%5==2?4:slot%5==3?2:3;}
12
+ function __fca_now():u64{
13
+ if(__fca_clock_failed)return PRL_SAFE+1;
14
+ const now=__request_clock();
15
+ if(now<__fca_origin||__request_expired){__fca_clock_failed=true;return PRL_SAFE+1;}
16
+ if(now>=__request_deadline)__request_expired=true;
17
+ return u64(now-__fca_origin);
18
+ }
19
+ function __fca_ok(ok:bool):bool{if(!ok)__pulse_fastly_fail(PULSE_ERROR_HOSTCALL,272,-1);return ok;}
20
+ function __fca_work():bool{return __fca_ok(__fca_now()<__FCA_DURATION);}
21
+ function __fca_start(index:i32):bool{
22
+ const ticket=__pulse_invocation_tickets[index];
23
+ if(ticket<=0||ticket>__FCA_COUNT||ticket!=__pulse_invocation_count)return __fca_ok(false);
24
+ __fca_frame=ticket-1;
25
+ if(__fetch_ra!==null)return __fca_work();
26
+ if(__request_deadline<0||__request_last_clock<0)return __fca_ok(false);
27
+ __fca_origin=__request_deadline-i64(__FCA_DURATION);
28
+ const l=new PulseResourceLedger(1,__FCA_RECORDS,__FCA_HANDLES,__FCA_BACKING,5*__FCA_COUNT+2,__FCA_COUNT,__FCA_DURATION,__FCA_STORAGE);__fetch_ra=l;
29
+ if(!l.begin(1,__fca_now(),5*__FCA_COUNT,2,__FCA_COUNT,true))return __fca_ok(false);
30
+ for(let i=0;i<__FCA_HANDLES;i++)if(!l.stageRecord(1,1,__fca_ns(i),1,i>=__FCA_HANDLES-2?3:1,0))return __fca_ok(false);
31
+ if(!l.stageRecord(1,2,0,1,1,__FCA_SCRATCH)||!l.stageRecord(1,2,0,1,1,__FCA_ENCODING)||!l.stageRecord(1,2,0,1,1,__FCA_READER)||!l.stageRecord(1,2,0,1,3,64)||!l.stageRecord(1,3,0,1,1,0)||!l.commit(1,__fca_now()))return __fca_ok(false);
32
+ for(let i=0;i<__FCA_RECORDS;i++)if(l.reservedSlot(i)!=i||l.reservedGeneration(i)!=1)return __fca_ok(false);
33
+ return __fca_ok(l.consume(1,__FCA_HANDLES,1,1,__FCA_SCRATCH,false,__fca_now())&&l.consume(1,__FCA_HANDLES+1,1,1,__FCA_ENCODING,false,__fca_now())&&l.consume(1,__FCA_HANDLES+2,1,1,__FCA_READER,false,__fca_now()));
34
+ }
35
+ function __fca_acquire(local:i32):bool{const s=__fca_slot(local);return __fetch_ra!==null&&__fca_ok(__fetch_ra!.beginAcquire(1,s,1,__fca_ns(s),1,local>=5,__fca_now()));}
36
+ function __fca_acquired(local:i32,status:i32,handle:i32):i32{const l=__fetch_ra!,s=__fca_slot(local);if(status==0)__fca_ok(l.acquired(1,s,1,__fca_ns(s),1,u32(handle)));else l.noAcquisition(1,s,1,__fca_ns(s),1,false);return status;}
37
+ function __fca_new(local:i32,out:usize):i32{if(!__fca_acquire(local))return 1;const ns=__fca_ns(__fca_slot(local));return __fca_acquired(local,ns==1?fastly_http_req_new(out):ns==2?fastly_http_resp_new(out):fastly_http_body_new(out),load<i32>(out));}
38
+ function __fca_dispose_slot(s:i32):i32{
39
+ if(__fetch_ra===null)return 1;const l=__fetch_ra!,ns=__fca_ns(s);
40
+ if(l.bank[s*8+5]!=PRL_LIVE||!l.beginDispose(1,s,1,ns,1,__fca_now()))return 1;
41
+ const h=i32(l.bank[s*8+6]);const status=ns==1?__pulse_fetch_close_request(h):ns==2?__pulse_fetch_close_response(h):__pulse_fetch_close_body(h);l.disposed(1,s,1,ns,1,status==0);return status;
42
+ }
43
+ function __fca_dispose(local:i32):i32{return __fca_dispose_slot(__fca_slot(local));}
44
+ function __fca_dispatch(request:i32,body:i32,backend:usize,length:i32,out:usize):i32{
45
+ if(!__fca_acquire(2)||!__fca_acquire(3)||!__fca_acquire(4))return 1;
46
+ const l=__fetch_ra!,b=__fca_frame*5;
47
+ if(!l.beginDispose(1,b,1,1,1,__fca_now())||!l.beginDispose(1,b+1,1,3,1,__fca_now()))return 1;
48
+ const status=fastly_http_req_send_async(request,body,backend,length,out);
49
+ l.disposed(1,b,1,1,1,status==0);l.disposed(1,b+1,1,3,1,status==0);return __fca_acquired(2,status,load<i32>(out));
50
+ }
51
+ function __fca_find(handle:i32,local:i32):bool{
52
+ if(__fetch_ra===null)return false;const l=__fetch_ra!;
53
+ for(let i=0;i<__FCA_COUNT;i++){const s=(i*5+local)*8;if(l.bank[s+5]==PRL_LIVE&&l.bank[s+6]==u32(handle)){__fca_frame=i;return true;}}
54
+ return false;
55
+ }
56
+ function __fca_poll(handle:i32,detail:usize,done:usize,response:usize,body:usize):i32{
57
+ if(!__fca_find(handle,2)||__fca_now()>=__FCA_DURATION)return 1;
58
+ const l=__fetch_ra!,b=__fca_frame*5;
59
+ const status=__pulse_fetch_poll(handle,detail,done,response,body),ready=load<i32>(done);
60
+ if(status!=0||(ready!=0&&ready!=1)){l.uncertain(1,b+2,1,4,1);l.noAcquisition(1,b+3,1,2,1,false);l.noAcquisition(1,b+4,1,3,1,false);return status==0?2:status;}
61
+ if(ready==1){l.transferred(1,b+2,1,4,1);__fca_acquired(3,0,load<i32>(response));__fca_acquired(4,0,load<i32>(body));}return 0;
62
+ }
63
+ function __fca_unreaped():i32{if(__fetch_ra===null)return 0;let n=0;for(let i=0;i<__FCA_COUNT;i++){const s=__fetch_ra!.bank[(i*5+2)*8+5];if(s!=PRL_FREE&&s!=PRL_RESERVED)n++;}return n;}
64
+ function __fca_cleanup(final:bool=false):void{
65
+ if(__fetch_ra===null)return;const l=__fetch_ra!;l.close(1);
66
+ for(let i=0;i<__FCA_COUNT;i++)if(!__fca_reaped[i]&&l.bank[(i*5+2)*8+5]==PRL_LIVE){__fca_reaped[i]=true;if(l.cleanupStep(1,false,__fca_now())){__pulse_send_error_detail_reset(__fca_io);store<i32>(__fca_io+20,0);__fca_poll(i32(l.bank[(i*5+2)*8+6]),__fca_io,__fca_io+20,__fca_io+24,__fca_io+28);}}
67
+ for(let s=0;s<__FCA_HANDLES;s++){const state=l.bank[s*8+5];if(state==PRL_RESERVED&&(s<__FCA_HANDLES-2||final))l.unused(1,s,1,__fca_ns(s),1);else if(state==PRL_LIVE&&__fca_ns(s)!=4)__fca_dispose_slot(s);}
68
+ }
69
+ function __fca_transfer(response:i32,body:i32):i32{
70
+ if(!__fca_find(response,3)||!__fca_work())return 1;
71
+ const l=__fetch_ra!,b=__fca_frame*5;
72
+ if(l.bank[(b+4)*8+6]!=u32(body)||!l.beginDispose(1,b+3,1,2,1,__fca_now())||!l.beginDispose(1,b+4,1,3,1,__fca_now()))return 1;
73
+ const status=fastly_http_resp_send_downstream(response,body,0);l.disposed(1,b+3,1,2,1,status==0);l.disposed(1,b+4,1,3,1,status==0);return status;
74
+ }
75
+ function __fca_write(body:i32,value:string):i32{
76
+ const bytes=__pulse_fastly_utf8(value);let offset=0;
77
+ while(offset<bytes.byteLength){if(!__fca_work())return 1;store<i32>(__fca_io+20,0);const status=fastly_http_body_write(body,changetype<usize>(bytes)+offset,bytes.byteLength-offset,0,__fca_io+20),n=load<i32>(__fca_io+20);if(status!=0)return status;if(n<=0||n>bytes.byteLength-offset)return 1;offset+=n;}return 0;
78
+ }
79
+ class __FCA_Header{ name:string;value:string|null;constructor(name:string,value:string|null){this.name=name;this.value=value;} }
80
+ function __fca_header(response:__PulseFastlyValue,name:string):string|null{
81
+ if(response.fetchHeaders===null)response.fetchHeaders=new Array<__FCA_Header>();
82
+ for(let i=0;i<response.fetchHeaders!.length;i++){const h=response.fetchHeaders![i];if(h.name==name)return h.value;}
83
+ if(__fca_header_id(name)<0||!__fca_find(response.responseHandle,3)||!__fca_work()){__fca_ok(false);return null;}
84
+ const value=__pulse_header_lookup(response.responseHandle,name,true);response.fetchHeaders!.push(new __FCA_Header(name,value));return value;
85
+ }
86
+ function __fca_json_type(response:__PulseFastlyValue):bool{
87
+ if(response.fetchJsonType==0){const value=__fca_header(response,'content-type');response.fetchJsonType=value!==null&&value!.length>0&&value!.toLowerCase().indexOf('json')>=0?1:-1;}
88
+ return response.fetchJsonType==1;
89
+ }
90
+ function __fca_status(response:__PulseFastlyValue):i32{
91
+ if(!__fca_find(response.responseHandle,3)||!__fca_work())return 0;
92
+ if(fastly_http_resp_status_get(response.responseHandle,__fca_io+20)!=0){__pulse_fastly_fail(PULSE_ERROR_HOSTCALL,34,-1);return 0;}
93
+ return host_value_number(load<i32>(__fca_io+20));
94
+ }
95
+ function __fca_error():bool {
96
+ if(__fetch_ra===null) return false;
97
+ const l=__fetch_ra!;
98
+ // Capture the primary driver failure before cleanup or protected delivery.
99
+ const status=__request_expired || __pulse_fastly_error_stage==271 ? 504
100
+ : __pulse_fastly_last_error==PULSE_ERROR_TRANSPORT ? 502 : 500;
101
+ if(l.primary==0) l.fail(1,u32(status));
102
+ __fca_cleanup();
103
+ if(!__pulse_response_owned() || !l.consume(1,__FCA_HANDLES+3,1,1,64,true,__fca_now())) return true;
104
+ if(__fca_new(5,__fca_io+24)!=0) return true;
105
+ const response=load<i32>(__fca_io+24);
106
+ if(__fca_new(6,__fca_io+28)!=0) return true;
107
+ const body=load<i32>(__fca_io+28);
108
+ if(__fca_now()>=__FCA_DURATION || fastly_http_resp_status_set(response,status)!=0) return true;
109
+ const header='content-typetext/plain; charset=utf-8';
110
+ for(let i=0;i<header.length;i++) store<u8>(__fca_exit+i,u8(header.charCodeAt(i)));
111
+ if(__fca_now()>=__FCA_DURATION || fastly_http_resp_header_append(response,__fca_exit,12,__fca_exit+12,header.length-12)!=0) return true;
112
+ const text=status==504?'Gateway Timeout':status==502?'Bad Gateway':'Internal Server Error';
113
+ for(let i=0;i<text.length;i++) store<u8>(__fca_exit+i,u8(text.charCodeAt(i)));
114
+ let offset=0;
115
+ while(offset<text.length) {
116
+ if(__fca_now()>=__FCA_DURATION) return true;
117
+ const result=fastly_http_body_write(body,__fca_exit+offset,text.length-offset,0,__fca_io+20);
118
+ const written=load<i32>(__fca_io+20);
119
+ if(result!=0 || written<=0 || written>text.length-offset) return true;
120
+ offset+=written;
121
+ }
122
+ if(!l.beginDispose(1,__FCA_HANDLES-2,1,2,1,__fca_now()) || !l.beginDispose(1,__FCA_HANDLES-1,1,3,1,__fca_now())) return true;
123
+ const sent=fastly_http_resp_send_downstream(response,body,0);
124
+ l.disposed(1,__FCA_HANDLES-2,1,2,1,sent==0); l.disposed(1,__FCA_HANDLES-1,1,3,1,sent==0);
125
+ return true;
126
+ }
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+ const fs=require('node:fs'),path=require('node:path');
3
+ const {resourceAdmission:admission}=require('@pulse-compute/wasm-contracts/provider/toolchain');
4
+ const {resourceLedgerSource}=require('@pulse-compute/wasm-runtime-core-as/compiler/canonical-native');
5
+ const {CANONICAL_EFFECT_INVOCATION_POLICY:policy}=require('@pulse-compute/wasm-contracts/handler/canonical-runtime');
6
+ function profile(plan,bindings,C){
7
+ if(!plan.effects.length || plan.effects.some(e=>e.kind!=='fetch') || plan.capabilities.includes('request.body.forward')
8
+ || plan.packages?.application || plan.callables?.length || plan.routing?.entries?.some(e=>e.kind==='error') || bindings.dynamicBackends
9
+ || !Number.isInteger(bindings.maxDurationMs)||bindings.maxDurationMs<1||bindings.maxDurationMs>30000)return null;
10
+ const B=plan.schemas?.registry?.maxBytes;
11
+ if(!Number.isInteger(B)||B<1||B>1073741823||!Number.isInteger(C)||C<9||C>1073741823)return null;
12
+ const fields=o=>o?.kind==='object'&&o.entries.every(e=>e.kind==='property'&&e.key?.kind==='literal'&&typeof e.key.value==='string')&&new Set(o.entries.map(e=>e.key.value)).size===o.entries.length?new Map(o.entries.map(e=>[e.key.value,e.value])):null;
13
+ const proof=require('./native-fetch-bounds.js').create(plan,C,B);
14
+ let encoding=0,caseBytes=0;
15
+ const literal=value=>({kind:'literal',value});
16
+ for(let i=0;i<plan.effects.length;i++){
17
+ const e=plan.effects[i],url=e.inputs.find(x=>x.name==='url')?.value,init=e.inputs.find(x=>x.name==='init')?.value;
18
+ const f=init&&init.kind!=='undefined'?fields(init):new Map(),backend=bindings.backends.find(b=>b.index===i)?.backend;
19
+ if(!f||[...f.keys()].some(k=>!['method','body','json','headers','timeoutMs'].includes(k))||!backend)return null;
20
+ const u=proof.string(url),method=proof.string(f.get('method')||literal('GET'));
21
+ if(!u||!method)return null;
22
+ const headers=f.has('headers')?fields(f.get('headers')):new Map();if(!headers)return null;
23
+ const values=[...headers.values()].map(v=>proof.string(v));if(values.some(v=>!v))return null;
24
+ const timeout=f.get('timeoutMs');if(timeout&&(timeout.kind!=='literal'||!Number.isSafeInteger(timeout.value)||timeout.value<=0))return null;
25
+ let body=proof.string(f.get('body')||literal('')),materialization=f.has('body')&&f.get('body').kind!=='literal'?128:0,extraHeader=0;
26
+ const json=f.has('body')&&!body?f.get('body'):!f.has('body')&&f.has('json')?f.get('json'):null;
27
+ if(json){const j=proof.json(json);if(!j)return null;body={chars:j.chars,bytes:3*j.chars};
28
+ // Quote buffers + quoted strings + final join, geometric parts-array
29
+ // growth, and a conservative pinned numeric formatter allowance.
30
+ materialization=8*j.chars+128*j.nodes+128+1024*j.numbers;
31
+ if(f.has('json')){extraHeader=Buffer.byteLength('content-typeapplication/json; charset=utf-8');materialization+=[...headers.keys()].reduce((n,k)=>n+4*k.length,0);}
32
+ }
33
+ if(!body)return null;
34
+ encoding=Math.max(encoding,u.bytes+Buffer.byteLength(backend)+(f.has('method')?9*method.chars:3)+body.bytes+extraHeader+[...headers.keys()].reduce((n,k)=>n+Buffer.byteLength(k),0)+values.reduce((n,v)=>n+v.bytes,0));
35
+ caseBytes=Math.max(caseBytes,(f.has('method')?6*method.chars:0)+materialization);
36
+ }
37
+ const headerNames=new Set(plan.schemas.registry.contentTypePolicy==='require-json'?['content-type']:[]);
38
+ let repeated=false,unsupported=false;
39
+ function visit(v){if(!v||typeof v!=='object')return;if(Array.isArray(v)){v.forEach(visit);return;}
40
+ if(/loop|while|for|map/.test(v.kind||'')||['helper-call','stage-call'].includes(v.kind))repeated=true;
41
+ if(v.kind==='method-call'&&v.receiver?.valueKind==='fetch-response'&&v.method==='header'){
42
+ const name=v.arguments?.[0];if(name?.kind!=='literal'||typeof name.value!=='string')unsupported=true;else headerNames.add(name.value);
43
+ }
44
+ Object.values(v).forEach(visit);
45
+ }
46
+ visit(plan.entry);visit(plan.handlers);visit(plan.helpers);visit(plan.stages);if(unsupported)return null;
47
+ const count=Math.min(policy.defaultMaxEffects,repeated?policy.defaultMaxEffects:plan.effects.length),header=[...headerNames].sort();
48
+ // Each response owns one cached decode per admitted name. Lowercase admission
49
+ // is also cached; repeated aliases never allocate another fetch-service copy.
50
+ const headerBytes=header.reduce((n,k)=>n+3*C+Buffer.byteLength(k)+11+(k==='content-type'?4*(C-1):0),0)+(header.length?48*header.length+32:0);
51
+ const scratch=12*count+(36+4*plan.effects.length)*count+52+count;
52
+ const backing=scratch+count*(encoding+caseBytes+3*B+5+headerBytes)+64;
53
+ const handles=5*count+2,records=handles+5,steps=5*count+2;
54
+ if(!Number.isSafeInteger(backing)||backing>9007199254740991)return null;
55
+ const fixed=(id,bytes)=>({id,kind:'fixed',bytes,copies:1});
56
+ const work={handles:[{namespace:'http.request',count},{namespace:'http.body',count:2*count},{namespace:'http.pending',count},{namespace:'http.response',count}],allocations:[fixed('service-scratch',scratch),fixed('encodings',count*(encoding+caseBytes)),fixed('reader-and-headers',count*(3*B+5+headerBytes))],records:1,cleanupSteps:0};
57
+ const exit={handles:[{namespace:'http.response',count:1},{namespace:'http.body',count:1}],allocations:[fixed('error-text',64)],records:0,cleanupSteps:2};
58
+ const limits={handles,backingBytes:backing,records,cleanupSteps:steps,effects:count};
59
+ const realization='fastly.native.fetch.cohorts-v1';
60
+ const result=admission.createPlan({version:admission.VERSION,realization,selected:[{site:'fetch-invocation',count:1,profile:{version:admission.PROFILE_VERSION,owner:'provider-fastly',operation:'bounded-fetch-invocation',realization,effects:count,work,cleanup:{handles:[],allocations:[],records:0,cleanupSteps:5*count},exclusions:['application-values','json-parser-and-schema-materialization','incoming-request-context','diagnostic-storage','driver-identity-storage','successful-downstream-response','allocator-metadata']}}],exit,occupied:Object.fromEntries(admission.DIMENSIONS.map(k=>[k,0])),limits});
61
+ if(!result.admitted||admission.DIMENSIONS.some(k=>result.required[k]!==limits[k]))throw Error('Fetch cohort profile drift');
62
+ return Object.freeze({...result,selection:'canonical-fetch-cohorts',executionIdentity:'private-single-invocation-wasm-instance',unprofiled:'unproved-inputs-dynamic-backends-mixed-capabilities-error-recovery-application-packages-child-graphs-and-other-targets',count,headerNames:header,headerBytes,scratchBytes:scratch,encodingBytes:count*(encoding+caseBytes),readerBytes:count*(3*B+5+headerBytes),storageBytes:1024+128*records,durationNs:bindings.maxDurationMs*1000000});
63
+ }
64
+ function source(p){return resourceLedgerSource(true)+`\nconst __FCA_COUNT:i32=${p.count};\nconst __FCA_HANDLES:i32=${p.required.handles};\nconst __FCA_RECORDS:i32=${p.required.records};\nconst __FCA_BACKING:u64=${p.required.backingBytes};\nconst __FCA_STORAGE:u32=${p.storageBytes};\nconst __FCA_DURATION:u64=${p.durationNs};\nconst __FCA_SCRATCH:u64=${p.scratchBytes};\nconst __FCA_ENCODING:u64=${p.encodingBytes};\nconst __FCA_READER:u64=${p.readerBytes};\nfunction __fca_header_id(name:string):i32 { ${p.headerNames.map((s,i)=>`if(name==${JSON.stringify(s)})return ${i};`).join('')} return -1; }\n`+fs.readFileSync(path.join(__dirname,'native-fetch-cohorts.as.ts'),'utf8');}
65
+ module.exports={profile,source};
@@ -15,11 +15,13 @@ const __pulse_fetch_retired = new Array<i32>()
15
15
  let __pulse_fetch_last_clock: i64 = -1
16
16
  let __pulse_fetch_retired_total: i32 = 0
17
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 }
18
+ export function pulse_fastly_fetch_unreaped(): i32 { return /*ACCOUNTING_UNREAPED*/ }
19
+ /*ACCOUNTING_CLOCK_STORAGE*/
19
20
  function __pulse_fetch_clock(index: i32): i64 {
20
- const out = new StaticArray<i64>(1)
21
+ const out = /*ACCOUNTING_CLOCK_OUT*/
21
22
  if (wasi_snapshot_preview1_clock_time_get(1, 1, changetype<usize>(out)) != 0
22
23
  || out[0] < 0 || out[0] < __pulse_fetch_last_clock) {
24
+ /*ACCOUNTING_CLOCK_FAILURE*/
23
25
  __pulse_fastly_fail(PULSE_ERROR_HOSTCALL, 272, index); return -1
24
26
  }
25
27
  __pulse_fetch_last_clock = out[0]; return out[0]
@@ -54,8 +56,10 @@ function __pulse_fetch_active(index: i32): bool {
54
56
  return __pulse_fastly_effect_kind(index) == 1 && __pulse_fastly_pending_mode[index] == PULSE_FASTLY_PENDING_ASYNC
55
57
  }
56
58
  function __pulse_fetch_has_deadlines(): bool {
57
- for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++)
59
+ for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) {
60
+ /*ACCOUNTING_FORCE_POLL*/
58
61
  if (__pulse_fetch_active(i) && __pulse_fetch_timeout[i] > 0) return true
62
+ }
59
63
  return false
60
64
  }
61
65
  function __pulse_fetch_complete(index: i32, outcome: i32, result: i32 = 0): void {
@@ -66,21 +70,22 @@ function __pulse_fetch_complete(index: i32, outcome: i32, result: i32 = 0): void
66
70
  function __pulse_fetch_retire(index: i32, outcome: i32): void {
67
71
  // The Fastly ABI has no pending-request cancellation hostcall. Retain explicit
68
72
  // ownership until invocation cleanup; never wait, retry, or claim rollback.
69
- __pulse_fetch_retired.push(__pulse_fastly_pending[index]); __pulse_fetch_retired_total++
73
+ /*ACCOUNTING_RETIRED*/ __pulse_fetch_retired_total++
70
74
  __pulse_fetch_complete(index, outcome)
71
75
  }
72
76
  function __pulse_fetch_retire_all(): void {
73
77
  for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i++) if (__pulse_fetch_active(i)) __pulse_fetch_retire(i, -2)
74
78
  }
75
79
  function __pulse_fetch_reap(): void {
80
+ /*ACCOUNTING_REAP*/
76
81
  const detail = new StaticArray<i32>(__PULSE_SEND_ERROR_DETAIL_WORDS), done = new StaticArray<i32>(1)
77
82
  const response = new StaticArray<i32>(1), body = new StaticArray<i32>(1)
78
83
  for (let i = __pulse_fetch_retired.length - 1; i >= 0; i--) {
79
84
  done[0] = 0
80
85
  __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))
86
+ const status = /*ACCOUNTING_POLL*/(__pulse_fetch_retired[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
82
87
  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]) }
88
+ if (status == 0 && done[0] == 1) { /*ACCOUNTING_LATE_CLOSE*/ }
84
89
  __pulse_fetch_retired[i] = __pulse_fetch_retired[__pulse_fetch_retired.length - 1]; __pulse_fetch_retired.pop()
85
90
  }
86
91
  // Unready handles remain platform-owned until host teardown. No guest task
@@ -114,7 +119,7 @@ function __pulse_fetch_wait(index: i32): i32 {
114
119
  if (__pulse_fetch_remaining(i, now) <= 0) { __pulse_fetch_retire(i, -1); continue }
115
120
  done[0] = 0
116
121
  __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))
122
+ const status = /*ACCOUNTING_POLL*/(__pulse_fastly_pending[i], changetype<usize>(detail), changetype<usize>(done), changetype<usize>(response), changetype<usize>(body))
118
123
  now = __pulse_fetch_clock(i)
119
124
  if (status != 0) {
120
125
  __pulse_transport_capture(i, status, 'poll', changetype<usize>(detail))
@@ -127,7 +132,7 @@ function __pulse_fetch_wait(index: i32): i32 {
127
132
  }
128
133
  if (now < 0 || __pulse_fetch_remaining(i, now) <= 0) {
129
134
  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)
135
+ /*ACCOUNTING_LATE_CLOSE*/; __pulse_fetch_complete(i, now < 0 ? -2 : -1)
131
136
  } else __pulse_fetch_retire(i, now < 0 ? -2 : -1)
132
137
  if (now < 0) { __pulse_fetch_retire_all(); return 0 }
133
138
  continue
@@ -2,13 +2,22 @@
2
2
  const fs = require('node:fs');
3
3
  const path = require('node:path');
4
4
  function enabled(plan) { return (plan.effects || []).some(effect => effect.kind === 'fetch'); }
5
- function runtimeSource(plan) {
5
+ function runtimeSource(plan, accounting) {
6
6
  if (!enabled(plan)) return `function __pulse_fetch_has_deadlines(): bool { return false }
7
7
  function __pulse_fetch_request_check(index: i32): bool { return true }
8
8
  function __pulse_fetch_request_remaining(): i32 { return 2147483647 }`;
9
- return fs.readFileSync(path.join(__dirname,'native-fetch-deadlines.as.ts'),'utf8');
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])');
10
19
  }
11
- function instrument(input, plan) {
20
+ function instrument(input, plan, accounting) {
12
21
  if (!enabled(plan)) return input;
13
22
  let source = input;
14
23
  const replace = (before,after) => {
@@ -18,7 +27,7 @@ function instrument(input, plan) {
18
27
  replace('const init = initIndex >= 0 ? __pulse_fastly_value(unchecked(payload.values[initIndex])) : new __PulseFastlyValue(); let method',
19
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');
20
29
  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');
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`);
22
31
  replace('function __pulse_fastly_wait_fetch(effectIndex: i32): i32 {',
23
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);');
24
33
  replace('function __pulse_invocation_close(): void {', 'function __pulse_invocation_close(): void { __pulse_fetch_reap();');
@@ -30,8 +30,8 @@ function __pulse_fetch_send_error(): bool {
30
30
  }
31
31
  `;
32
32
  }
33
- function instrument(source) {
34
- const marker = 'function __pulse_fastly_jwt_send_error(): void {';
33
+ function instrument(source, lifecycle) {
34
+ const marker = `function ${lifecycle?.legacyName('__pulse_fastly_jwt_send_error') || '__pulse_fastly_jwt_send_error'}(): void {`;
35
35
  if (!source.includes(marker)) throw new Error('Fastly fetch error boundary drift.');
36
36
  return source.replace(marker, marker + ' if (__pulse_fetch_send_error()) return;');
37
37
  }