Bounded WEB HTTP conveyor with replay-safe streaming downlink added

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-10-03 16:02:18 +03:00
parent c4555e25f3
commit 823c6cbc7f
39 changed files with 2186 additions and 237 deletions
+251
View File
@@ -0,0 +1,251 @@
'use strict';
const assert=require('node:assert/strict');
const fs=require('node:fs');
const vm=require('node:vm');
const path=require('node:path');
function renderedPage(){
if(process.argv.includes('--stdin'))return fs.readFileSync(0,'utf8');
let page=fs.readFileSync(path.join(__dirname,'document.html'),'utf8');
const modules={RESPONSE:'response',REQUEST:'request',BUFFER:'buffers',RECOVERY:'recovery',CONVEYOR:'conveyor',DOWNLINK:'downlink'};
page=page.replace('__DIAGNOSTIC_RUNTIME__','');
for(const [key,file] of Object.entries(modules))if(page.includes('__'+key+'_RUNTIME__'))page=page.replace('__'+key+'_RUNTIME__',()=>fs.readFileSync(path.join(__dirname,file+'.js'),'utf8'));
page=page.replace('__RUNTIME__',()=>fs.readFileSync(path.join(__dirname,'runtime.js'),'utf8'));
const values={BOOTSTRAP:'A'.repeat(43),HOST:'proxy.example.com',BASE_PREFIX:'',CARRIER_METHOD:'POST',NEGOTIATION_ENABLED:'true',CANDIDATE_COUNT:4,CARRIER_DEADLINES:'3,5,8,12',LONG_POLL_SECS:25,BRIDGE_REQUEST_SECS:10,BRIDGE_RETRY_SECS:90,BRIDGE_RECOVERY_SECS:15,WEBSOCKET_OPEN_SECS:15,RECONNECT_GRACE_SECS:120,CARRIER_PROBE_COALESCE_MS:0,BATCH_LIMIT:2097152,QUEUE_LIMIT:33554432,QUEUE_ITEMS:16384,MAX_STREAMS:1024,STATUS_FUNCTION:"state=>{if(port&&!closed)port.postMessage({t:'status',state})}",HELLO_TIMEOUT_CALLBACK:"()=>fail('timeout')",PAGEHIDE_CALLBACK:'()=>close(true)'};
return page.replace(/__([A-Z_]+)__/g,(all,key)=>key.startsWith('DIAGNOSTIC_')?'':String(values[key]??''));
}
function frame(type,id=0,payload=[]){
const data=new Uint8Array(8+payload.length),view=new DataView(data.buffer);
data[0]=type;data[1]=id>>>16;data[2]=id>>>8;data[3]=id;view.setUint32(4,payload.length);data.set(payload,8);return data.buffer;
}
function join(...frames){const data=new Uint8Array(frames.reduce((n,f)=>n+f.byteLength,0));let offset=0;for(const f of frames){data.set(new Uint8Array(f),offset);offset+=f.byteLength}return data.buffer}
async function flush(){for(let i=0;i<40;i++)await Promise.resolve()}
function environment(page,android=false){
let now=1000,nextTimer=1;const timers=new Map(),events=new Map(),requests=[],received=[];
const port={onmessage:null,start(){},close(){},postMessage(value,transfer){received.push(structuredClone(value,{transfer:transfer||[]}))}};
const context={ArrayBuffer,Uint8Array,DataView,TextDecoder,TextEncoder,URL,Headers,AbortController,ReadableStream,structuredClone,console,
location:{hash:android?'#android='+'A'.repeat(43):'',pathname:'/',search:'?bridge=test'},history:{replaceState(){}},parent:{},
performance:{now:()=>now},Date:class extends Date{static now(){return now}},document:{visibilityState:'visible',addEventListener(){}},
addEventListener:(name,fn)=>events.set(name,fn),setTimeout:(fn,delay)=>{const id=nextTimer++;timers.set(id,{fn,at:now+delay});return id},clearTimeout:id=>timers.delete(id),
fetch:(url,options)=>new Promise((resolve,reject)=>{const request={url,options,resolve,reject};requests.push(request);options.signal?.addEventListener('abort',()=>reject(new Error('aborted')),{once:true})}),
WebSocket:class {static OPEN=1;static CLOSING=2;constructor(){throw new Error('unexpected websocket')}}
};
if(android)context.TelegramWebProxy={onmessage:null,postMessage(value){received.push(typeof value==='string'?JSON.parse(value):structuredClone(value))}};
vm.createContext(context);
for(const match of page.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g))vm.runInContext(match[1],context);
if(!android)events.get('message')({source:context.parent,origin:'http://127.0.0.1:12345',data:{t:'tproxy-init',v:1},ports:[port]});
const send=data=>android?context.TelegramWebProxy.onmessage({data}):port.onmessage({data});
const pending=suffix=>requests.filter(r=>r.url.endsWith(suffix)&&!r.answered);
const answer=(request,status,body=null,headers={})=>{request.answered=true;request.resolve({status,headers:new Headers(headers),body:body===null?null:new ReadableStream({start(c){c.enqueue(new Uint8Array(body));c.close()}})})};
return {context,send,pending,answer,received,requests,async tick(ms){const until=now+ms;for(;;){const entry=[...timers].filter(([,t])=>t.at<=until).sort((a,b)=>a[1].at-b[1].at)[0];if(!entry)break;now=entry[1].at;timers.delete(entry[0]);entry[1].fn();await flush()}now=until;await flush()},close(){events.get('pagehide')();}};
}
async function session(page,carrier='https',window=1,android=false){
const env=environment(page,android);env.send(frame(16,0,[1]));await flush();
const request=env.pending('/api/v1/session')[0];assert.ok(request,'session requested');
const headers={'X-Session-Token':'B'.repeat(43),'X-Down-Cursor':'0','X-Carrier-Mode':carrier,'X-Carrier-Attempt':'1','X-Carrier-Candidate-Count':'4','X-Carrier-Deadline':'12','X-Carrier-State':'provisional'};
if(window!==null)headers['X-Telemt-Up-Window']=String(window);
env.answer(request,200,frame(17),headers);await flush();return env;
}
const tests=[];
function test(name,run){tests.push({name,run})}
function ack(env,request){env.answer(request,204,null,{'X-Up-Ack':request.options.headers['X-Up-Seq']})}
function binary(env){return env.received.filter(value=>value instanceof ArrayBuffer&&new Uint8Array(value)[0]!==17)}
function streaming(env,request,next='1',length=null){
let controller;const body=new ReadableStream({start(value){controller=value}}),headers={'X-Down-Cursor':next};if(length!==null)headers['Content-Length']=String(length);
request.answered=true;request.resolve({status:200,headers:new Headers(headers),body});return controller;
}
test('already queued OPEN and DATA share the zero-wait startup request',async page=>{
const env=await session(page);const body=join(frame(1,1),frame(2,1,[7,8]));env.send(body);await flush();
assert.equal(env.pending('/api/v1/up')[0].options.body.byteLength,body.byteLength);env.close();await flush();
});
test('negotiated conveyor sends DATA before the OPEN acknowledgement',async page=>{
for(const android of [false,true])for(const carrier of ['https','https-lanes']){
const env=await session(page,carrier,4,android);env.send(frame(1,1));await flush();env.send(frame(2,1,[9]));await flush();
assert.equal(env.pending('/api/v1/up').length,2,carrier+' startup must not wait for ACK1');env.close();await flush();
}
});
test('unsupported server and disabled conveyor remain serialized',async page=>{
for(const window of [null,1]){
const env=await session(page,'https',window);env.send(frame(1,1));await flush();env.send(frame(2,1,[9]));await flush();
assert.equal(env.pending('/api/v1/up').length,1);env.close();await flush();
}
});
test('lost ACK1 cannot slide the window after ACK2 through ACK4',async page=>{
const env=await session(page,'https',4);env.send(frame(1,1));await flush();
for(let i=0;i<6;i++){env.send(frame(2,1,[i]));await flush()}
const up=env.pending('/api/v1/up');assert.equal(up.length,4);
for(const request of up.slice(1)){ack(env,request);await flush()}
assert.equal(env.requests.filter(r=>r.url.endsWith('/api/v1/up')).length,4);
ack(env,up[0]);await flush();
const next=env.pending('/api/v1/up')[0];assert.equal(next.options.headers['X-Up-Seq'],'5');assert.equal(next.options.headers['X-Telemt-Up-Confirmed'],'4');
env.close();await flush();
});
test('early downlink cannot deliver native data before a verified commit',async page=>{
const env=await session(page,'https',4);env.send(frame(1,1));await flush();
const down=env.pending('/api/v1/down')[0];assert.ok(down,'down starts before ACK');const data=frame(2,1,[4]);
const stream=streaming(env,down,'1',data.byteLength);stream.enqueue(new Uint8Array(data));await flush();assert.equal(binary(env).length,0);
ack(env,env.pending('/api/v1/up')[0]);await flush();assert.equal(binary(env).length,1);
assert.equal(env.requests.filter(r=>r.url.endsWith('/api/v1/down')).length,1,'cursor cannot advance before EOF');
stream.close();await flush();assert.equal(env.pending('/api/v1/down')[0].options.headers['X-Down-Cursor'],'1');env.close();await flush();
});
test('DATA and WINDOW prefixes are delivered once across a mid-body network failure',async page=>{
for(const carrier of ['https','https-lanes']){
const env=await session(page,carrier,4);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const first=frame(2,1,[1,2]),credit=frame(4,1,[0,0,0,2]),last=frame(3,1),body=join(first,credit,last);
const stream=streaming(env,env.pending('/api/v1/down')[0],'1',body.byteLength);
stream.enqueue(new Uint8Array(join(first,credit)));await flush();assert.equal(binary(env).length,2);
stream.error(new Error('connection lost'));await flush();
const replay=env.pending('/api/v1/down')[0];assert.ok(replay);assert.equal(replay.options.headers['X-Down-Cursor'],'0');
env.answer(replay,200,body,{'X-Down-Cursor':'1','Content-Length':String(body.byteLength)});await flush();
assert.deepEqual(binary(env).map(data=>new Uint8Array(data)[0]),[2,4,3]);
assert.equal(env.pending('/api/v1/down')[0].options.headers['X-Down-Cursor'],'1');
assert.equal(env.requests.filter(r=>r.url.endsWith('/api/v1/down')).length,3,'successful replay is consumed without another old-cursor request');env.close();await flush();
}
});
test('changed replay prefix fails closed without duplicate native frames',async page=>{
const env=await session(page,'https',4);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const data=frame(2,1,[7]),tail=frame(2,1,[8]),body=join(data,tail),stream=streaming(env,env.pending('/api/v1/down')[0],'1',body.byteLength);
stream.enqueue(new Uint8Array(data));await flush();stream.error(new Error('lost'));await flush();
const changed=join(frame(2,1,[9]),tail);env.answer(env.pending('/api/v1/down')[0],200,changed,{'X-Down-Cursor':'1','Content-Length':String(changed.byteLength)});await flush();
assert.equal(binary(env).length,1);assert.ok(env.received.some(value=>value&&value.t==='close'));env.close();await flush();
});
test('partial frame boundaries never escape to native and cancellation owns every lease',async page=>{
const env=await session(page,'https-lanes',4,true);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const data=frame(2,1,[1,2,3]),stream=streaming(env,env.pending('/api/v1/down')[0],'1',data.byteLength);
stream.enqueue(new Uint8Array(data,0,5));await flush();assert.equal(binary(env).length,0);
stream.enqueue(new Uint8Array(data,5));await flush();assert.equal(binary(env).length,1);
env.close();await flush();assert.equal(binary(env).length,1);
});
test('post-commit lane batching keeps OPEN and DATA from one native message together',async page=>{
const env=await session(page,'https-lanes',1);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const body=join(frame(1,2),frame(2,2,[7]));env.send(body);await flush();
assert.equal(env.pending('/api/v1/up')[0].options.body.byteLength,body.byteLength);env.close();await flush();
});
test('page-owned HTTP method and identity survive frozen retries',async page=>{
const expected=/const carrierMethod='(POST|PUT)'/.exec(page)[1],env=await session(page,'https',4);
assert.equal(env.requests.find(r=>r.url.endsWith('/api/v1/session')).options.method,'POST');
env.send(frame(1,1));await flush();const first=env.pending('/api/v1/up')[0];ack(env,first);await flush();
env.send(frame(2,1,[7]));await flush();const original=env.pending('/api/v1/up')[0];
original.answered=true;original.reject(new Error('lost'));await flush();const replay=env.pending('/api/v1/up')[0];
assert.equal(replay.options.body,original.options.body);assert.deepEqual(replay.options.headers,original.options.headers);
assert.equal(replay.options.method,expected);ack(env,replay);await flush();
for(const request of env.requests.filter(r=>/\/api\/v1\/(up|down)$/.test(r.url)))assert.equal(request.options.method,expected);
env.close();await flush();assert.equal(env.requests.find(r=>r.options.method==='DELETE').options.method,'DELETE');
});
test('RTT scales time but adds no OPEN ACK barrier or coalescing timer',async page=>{
for(const rtt of [40,100,400]){
const env=await session(page,'https',4);env.send(frame(1,1));await flush();await env.tick(1);
env.send(frame(2,1,[5]));await flush();assert.equal(env.pending('/api/v1/up').length,2);
await env.tick(rtt-1);for(const request of env.pending('/api/v1/up'))ack(env,request);await flush();
assert.equal(env.received.filter(value=>value instanceof ArrayBuffer&&new Uint8Array(value)[0]===17).length,1);
assert.ok(env.received.filter(value=>value&&value.t==='status').every(value=>Object.keys(value).sort().join(',')==='state,t'));
env.close();await flush();
}
});
test('simultaneous failed lane polls each consume their own exact replay',async page=>{
const env=await session(page,'https-lanes',4);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
env.send(frame(1,2));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const streams=[];
for(const request of env.pending('/api/v1/down')){
const id=Number(request.options.headers['X-Lane-ID']),data=frame(2,id,[id]),body=join(data,frame(4,id,[0,0,0,1]));
const response=streaming(env,request,'1',body.byteLength);response.enqueue(new Uint8Array(data));streams.push({id,response,body});
}
await flush();assert.equal(binary(env).length,2);for(const item of streams)item.response.error(new Error('lost'));await flush();
for(let round=0;round<3;round++){
for(const request of env.pending('/api/v1/down').filter(r=>r.options.headers['X-Down-Cursor']==='0')){
const body=streams.find(item=>String(item.id)===request.options.headers['X-Lane-ID']).body;
env.answer(request,200,body,{'X-Down-Cursor':'1','Content-Length':String(body.byteLength)});
}await flush();
}
assert.equal(binary(env).length,4);assert.equal(env.pending('/api/v1/down').length,2);
assert.ok(env.pending('/api/v1/down').every(r=>r.options.headers['X-Down-Cursor']==='1'));env.close();await flush();
});
test('incremental decoder rejects changed cursor, invalid WINDOW and cross-lane frames',async page=>{
for(const invalid of ['cursor','credit','lane']){
const env=await session(page,'https-lanes',4);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const body=invalid==='credit'?frame(4,1,[0,0,0,0]):frame(2,invalid==='lane'?2:1,[1]);
env.answer(env.pending('/api/v1/down')[0],200,body,{'X-Down-Cursor':invalid==='cursor'?'2':'1','Content-Length':String(body.byteLength)});await flush();
assert.equal(binary(env).length,0);assert.ok(env.received.some(value=>value&&value.t==='close'));env.close();await flush();
}
});
test('a replay without Content-Length cannot truncate an already known batch',async page=>{
const env=await session(page,'https',4);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const prefix=frame(2,1,[1]),tail=frame(2,1,[2]),body=join(prefix,tail),response=streaming(env,env.pending('/api/v1/down')[0],'1',body.byteLength);
response.enqueue(new Uint8Array(prefix));await flush();response.error(new Error('lost'));await flush();
env.answer(env.pending('/api/v1/down')[0],200,prefix,{'X-Down-Cursor':'1'});await flush();
assert.equal(binary(env).length,1);assert.ok(env.received.some(value=>value&&value.t==='close'));
assert.ok(!env.requests.some(r=>r.url.endsWith('/api/v1/down')&&r.options.headers['X-Down-Cursor']==='1'));env.close();await flush();
});
test('a pipelined startup flight adopts the recovery budget once another ACK commits',async page=>{
const env=await session(page,'https',4);env.send(frame(1,1));await flush();env.send(frame(2,1,[7]));await flush();
const [head,tail]=env.pending('/api/v1/up');ack(env,head);await flush();tail.answered=true;tail.reject(new Error('lost'));await flush();
const replay=env.pending('/api/v1/up')[0];assert.ok(replay,'post-commit recovery replays immediately instead of staying in the startup backoff');
assert.equal(replay.options.body,tail.options.body);ack(env,replay);await flush();env.close();await flush();
});
test('candidate rollback restores every unacknowledged OPEN and DATA without a second WELCOME',async page=>{
const env=await session(page,'https',4);env.send(frame(1,1));await flush();env.send(frame(2,1,[7]));await flush();
const retired=env.requests.filter(r=>/\/api\/v1\/(up|down)$/.test(r.url));await env.tick(3001);
const next=env.pending('/api/v1/session')[0];assert.ok(next,'candidate fallback creates a new session');
env.answer(next,200,frame(17),{'X-Session-Token':'C'.repeat(43),'X-Down-Cursor':'0','X-Carrier-Mode':'https-lanes','X-Carrier-Attempt':'2','X-Carrier-Candidate-Count':'4','X-Carrier-Deadline':'12','X-Carrier-State':'provisional','X-Telemt-Up-Window':'4'});await flush();
const first=env.requests.find(r=>r.url.endsWith('/api/v1/up')&&r.options.headers.Authorization==='Bearer '+'C'.repeat(43));
assert.ok(first);assert.deepEqual(new Uint8Array(first.options.body),new Uint8Array(join(frame(1,1),frame(2,1,[7]))));
assert.ok(retired.every(r=>r.options.signal.aborted));assert.equal(env.received.filter(value=>value instanceof ArrayBuffer&&new Uint8Array(value)[0]===17).length,1);
ack(env,first);await flush();env.close();await flush();
});
test('downlink prefix reservations are bounded, cancellable and released on retirement',async page=>{
const env=environment(page),budget=env.context.TelemtBridgeDownlink.budget(()=>16),first=await budget.acquire(16);
const controller=new AbortController();let acquired=false;
const parked=budget.acquire(8,controller.signal).then(release=>{acquired=true;release()},()=>{});await flush();assert.equal(acquired,false);
controller.abort();await parked;first();assert.equal(budget.used(),0);
const release=await budget.acquire(16);release();release();assert.equal(budget.used(),0);env.close();await flush();
});
test('two interrupted and differently chunked replays never duplicate DATA WINDOW or CLOSE',async page=>{
const env=await session(page,'https',4);env.send(frame(1,1));await flush();ack(env,env.pending('/api/v1/up')[0]);await flush();
const data=frame(2,1,[1]),credit=frame(4,1,[0,0,0,1]),close=frame(3,1),body=join(data,credit,close);
const first=streaming(env,env.pending('/api/v1/down')[0],'1',body.byteLength);
first.enqueue(new Uint8Array(data));await flush();first.error(new Error('lost once'));await flush();
const second=streaming(env,env.pending('/api/v1/down')[0],'1',body.byteLength);
const prefix=new Uint8Array(join(data,credit));second.enqueue(prefix.subarray(0,3));second.enqueue(prefix.subarray(3));await flush();
assert.deepEqual(binary(env).map(value=>new Uint8Array(value)[0]),[2,4]);second.error(new Error('lost twice'));await flush();await env.tick(1000);
const last=streaming(env,env.pending('/api/v1/down')[0],'1',body.byteLength);
for(const byte of new Uint8Array(body))last.enqueue(new Uint8Array([byte]));last.close();
for(let i=0;i<20&&(!env.pending('/api/v1/down').length);i++)await flush();
assert.deepEqual(binary(env).map(value=>new Uint8Array(value)[0]),[2,4,3]);assert.equal(env.pending('/api/v1/down')[0].options.headers['X-Down-Cursor'],'1');
env.close();await flush();
});
test('multiple precommit lanes survive fallback without losing stream ordering',async page=>{
const env=await session(page,'https-lanes',4);env.send(join(frame(1,1),frame(1,2)));await flush();
env.send(join(frame(2,2,[2]),frame(2,1,[1])));await flush();await env.tick(3001);
const next=env.pending('/api/v1/session')[0];env.answer(next,200,frame(17),{'X-Session-Token':'C'.repeat(43),'X-Down-Cursor':'0','X-Carrier-Mode':'https','X-Carrier-Attempt':'2','X-Carrier-Candidate-Count':'4','X-Carrier-Deadline':'12','X-Carrier-State':'provisional','X-Telemt-Up-Window':'4'});await flush();
const requests=env.requests.filter(r=>r.url.endsWith('/api/v1/up')&&r.options.headers.Authorization==='Bearer '+'C'.repeat(43));
const frames=[];for(const request of requests){const bytes=new Uint8Array(request.options.body);for(let offset=0;offset<bytes.length;){const view=new DataView(bytes.buffer,offset);frames.push([bytes[offset],bytes[offset+3]]);offset+=8+view.getUint32(4)}}
for(const id of [1,2])assert.deepEqual(frames.filter(value=>value[1]===id),[[1,id],[2,id]]);
assert.equal(frames.length,4);env.close();await flush();
});
test('receiver retirement releases shared prefix capacity without exposing a partial frame',async page=>{
const env=environment(page),module=env.context.TelemtBridgeDownlink,budget=module.budget(()=>18),received=[];
function receiver(id){
let cursor='0',stream;const controller=new AbortController(),response={status:200,headers:new Headers({'X-Down-Cursor':'1','Content-Length':'18'}),body:new ReadableStream({start(value){stream=value}})};
const owner=module.create({failure:(reason,message)=>Object.assign(new Error(message),{telemtReason:reason}),alive:()=>true,cursor:()=>cursor,lane:id,limit:()=>18,budget,cancel(){},ready:()=>Promise.resolve(),deliver:data=>received.push(new Uint8Array(data)),advance:value=>{cursor=value},finished(){}});
const result=owner.read(response,controller.signal).catch(()=>{});return {owner,result,stream,controller,cursor:()=>cursor};
}
const a=receiver(1);a.stream.enqueue(new Uint8Array(frame(2,1,[1])));await flush();
const b=receiver(2);b.stream.enqueue(new Uint8Array(join(frame(2,2,[2]),frame(2,2,[3]))));b.stream.close();await flush();
assert.equal(budget.used(),18);assert.equal(received.length,1);assert.equal(b.cursor(),'0');
a.owner.close();a.controller.abort();a.stream.error(new Error('retired'));await a.result;await b.result;
assert.equal(received.length,3);assert.equal(b.cursor(),'1');assert.equal(budget.used(),0);
b.owner.close();env.close();await flush();
});
(async()=>{const page=renderedPage();let failed=0;for(const {name,run} of tests){let timer;try{await Promise.race([run(page),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('test made no bounded progress')),5000)})]);console.log('ok - '+name)}catch(error){failed++;console.error('not ok - '+name+'\n'+error.stack)}finally{clearTimeout(timer)}}if(failed)process.exitCode=1})();
+26
View File
@@ -0,0 +1,26 @@
use std::io::Write;
use std::process::{Command, Stdio};
/// Exercises the actual generated page with deterministic network and native-boundary doubles.
pub(super) fn run(page: &super::BridgePage) {
let mut child = Command::new("node")
.args(["-e", include_str!("behavior_tests.js"), "--", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Node.js 22+ is required for executable WEB bridge regressions");
child
.stdin
.take()
.unwrap()
.write_all(page.body.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"bridge regressions failed:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
+106
View File
@@ -0,0 +1,106 @@
(()=>{'use strict';
// HTTP request ownership is separate from the native WINDOW credit protocol.
function create(settings){
const channels=new Map(),window=settings.window,token=settings.token,downBudget=globalThis.TelemtBridgeDownlink.budget(settings.queueLimit);
let closed=false,order=0,ready=settings.committed(),resolveReady;
const readyPromise=new Promise(resolve=>{resolveReady=resolve});if(ready)resolveReady();
const alive=channel=>!closed&&!channel.closed&&settings.alive(channel.lane);
const markReady=()=>{if(!ready){ready=true;resolveReady();settings.commit()}};
function waitReady(signal){
if(ready)return Promise.resolve();
return new Promise((resolve,reject)=>{const abort=()=>reject(new Error('carrier retired'));if(signal&&signal.aborted){abort();return}if(signal)signal.addEventListener('abort',abort,{once:true});readyPromise.then(()=>{if(signal)signal.removeEventListener('abort',abort);resolve()})});
}
function get(id,type){
let channel=channels.get(id);if(channel)return channel;
const lane=id===null?null:settings.lane(id,type);if(id!==null&&!lane)return null;
channel={id,lane,pending:[],flights:new Map(),acks:new Set(),next:1,confirmed:0,cursor:'0',controller:null,polling:false,closed:false};
channel.receiver=globalThis.TelemtBridgeDownlink.create({
failure:settings.failure,alive:()=>alive(channel),cursor:()=>channel.cursor,lane:id,limit:settings.batchLimit,budget:downBudget,
cancel:settings.cancel,ready:waitReady,deliver:settings.deliver,advance:next=>{channel.cursor=next},finished:()=>{if(lane)settings.finish(lane,false)}
});channels.set(id,channel);return channel;
}
function enqueue(data,defer){
if(closed)return;
const entries=settings.lanes?settings.buffers.splitFrames(data):[{id:null,type:0,data}],touched=new Set();
for(const entry of entries){
const channel=get(entry.id,entry.type);if(!channel)continue;
if(!settings.buffers.reserve(entry.data,channel.lane))throw settings.failure('capacity','carrier queue full');
channel.pending.push(entry.data);touched.add(channel);
}
if(!defer)for(const channel of touched)pump(channel);
}
function checkAck(response,sequence){
if(response.status!==204)throw settings.failure('http','uplink rejected');
if(response.headers.get('X-Up-Ack')!==String(sequence))throw settings.failure('protocol','uplink acknowledgement rejected');
}
function acknowledge(channel,sequence,lease){
if(!alive(channel)||channel.flights.get(sequence)!==lease)return;
settings.buffers.settleBatch(lease);channel.flights.delete(sequence);channel.acks.add(sequence);
while(channel.acks.delete(channel.confirmed+1))channel.confirmed++;
markReady();settings.traffic(lease.total,0);pump(channel);
}
async function send(channel,sequence,lease){
const headers={'X-Up-Seq':String(sequence)};if(channel.id!==null)headers['X-Lane-ID']=String(channel.id);
if(window>1)headers['X-Telemt-Up-Confirmed']=String(channel.confirmed);
const frozen=settings.options(settings.method,token,lease.body,headers,lease.controller.signal);
try{
while(alive(channel)&&!lease.cancelled){
try{const response=await settings.request('/api/v1/up',frozen,null,()=>ready?1:9);if(!alive(channel))return;checkAck(response,sequence);acknowledge(channel,sequence,lease);return}
catch(error){
if(!alive(channel)||lease.cancelled)return;
if(!ready){settings.failed(error);return}
let replayed=false;
const recovered=await settings.recover(error,async(signal,remaining)=>{
const response=await settings.request('/api/v1/up',Object.assign({},frozen,{signal}),remaining,2);
if(!alive(channel)||lease.cancelled)return;checkAck(response,sequence);replayed=true;acknowledge(channel,sequence,lease);
});
if(!recovered||!alive(channel)||replayed)return;
}
}
}catch(error){if(alive(channel)&&!lease.cancelled)settings.failed(error)}
}
function pump(channel){
while(alive(channel)&&channel.pending.length&&channel.next<=channel.confirmed+window){
const lease=settings.buffers.takeBatch(channel.pending,channel.lane),sequence=channel.next++;
lease.controller=new AbortController();lease.order=order++;channel.flights.set(sequence,lease);send(channel,sequence,lease);
if(!channel.polling)poll(channel);
}
}
async function poll(channel){
if(channel.polling||!alive(channel))return;channel.polling=true;
try{
while(alive(channel)){
const cursor=channel.cursor,headers={'X-Down-Cursor':cursor};if(channel.id!==null)headers['X-Lane-ID']=String(channel.id);
channel.controller=new AbortController();const frozen=settings.options(settings.method,token,null,headers,channel.controller.signal);
try{
const response=await settings.request('/api/v1/down',frozen,null,1,channel.receiver.read);
if(!alive(channel))return;if(response.status!==200&&response.status!==204)throw settings.failure('http','downlink rejected');
if(ready)settings.connected();
}catch(error){
if(!alive(channel))return;
if(!ready){settings.failed(error);return}
const recovered=await settings.recover(error,async(signal,remaining)=>{
const response=await settings.request('/api/v1/down',Object.assign({},frozen,{signal}),remaining,2,channel.receiver.read);
if(response.status!==200&&response.status!==204)throw settings.failure('http','downlink replay rejected');
});
if(!recovered||!alive(channel))return;
}
}
}catch(error){if(alive(channel))settings.failed(error)}
finally{channel.polling=false;channel.controller=null}
}
function retire(channel,restore){
channel.closed=true;if(channel.controller)channel.controller.abort();channel.receiver.close();
const saved=[];
for(const lease of channel.flights.values()){if(restore)saved.push({order:lease.order,data:lease.body});settings.buffers.cancelBatch(lease)}
channel.flights.clear();channel.acks.clear();
for(const data of channel.pending)if(restore)saved.push({order:order++,data});
settings.buffers.releasePending(channel.pending,channel.lane);return saved;
}
return {enqueue,flush(){for(const channel of channels.values())pump(channel)},finish(lane){const channel=channels.get(lane.id);if(channel){channels.delete(lane.id);retire(channel,false)}},close(restore){
if(closed)return [];closed=true;const saved=[];for(const channel of channels.values())saved.push(...retire(channel,restore));channels.clear();
return saved.sort((a,b)=>a.order-b.order).map(entry=>entry.data);
}};
}
globalThis.TelemtBridgeConveyor=Object.freeze({create});
})();
+6
View File
@@ -20,6 +20,12 @@ __BUFFER_RUNTIME__
__RECOVERY_RUNTIME__
</script>
<script nonce="__NONCE__">
__DOWNLINK_RUNTIME__
</script>
<script nonce="__NONCE__">
__CONVEYOR_RUNTIME__
</script>
<script nonce="__NONCE__">
__RUNTIME__
</script>
</body>
+86
View File
@@ -0,0 +1,86 @@
(()=>{'use strict';
// Prefix ownership survives HTTP retries, but never a session or lane replacement.
function budget(limit){
let used=0;const waiters=[];
function wake(){for(const next of waiters.slice())next()}
function acquire(size,signal){
if(size>limit())return Promise.reject(new Error('downlink budget exceeded'));
return new Promise((resolve,reject)=>{
const remove=()=>{const at=waiters.indexOf(tryAcquire);if(at>=0)waiters.splice(at,1);if(signal)signal.removeEventListener('abort',abort)};
const abort=()=>{remove();reject(new Error('downlink aborted'))};
const tryAcquire=()=>{if(signal&&signal.aborted){abort();return}if(size<=limit()-used){remove();used+=size;let released=false;resolve(()=>{if(!released){released=true;used-=size;wake()}})}};
waiters.push(tryAcquire);if(signal)signal.addEventListener('abort',abort,{once:true});tryAcquire();
});
}
return {acquire,used:()=>used};
}
function create(settings){
let transaction=null,closed=false;
const protocol=message=>settings.failure('protocol',message);
const current=signal=>!closed&&settings.alive()&&!(signal&&signal.aborted);
function dispose(){if(transaction){transaction.release();transaction=null}}
function shape(type,id,size){
if(type===2)return id!==0&&size>0&&size<=1048576;
if(type===3)return id!==0&&size===0;
if(type===4)return id!==0&&size===4;
if(type===5)return id===0&&size<=64;
return type===31&&id===0&&size===0;
}
async function read(response,signal){
if(!current(signal))throw new Error('downlink retired');
if(response.status===204){
settings.cancel(response);if(transaction)throw protocol('missing replay batch');
const cursor=response.headers.get('X-Down-Cursor');
if(cursor!==null&&cursor!==settings.cursor())throw protocol('changed empty cursor');
if(response.headers.get('X-Lane-Closed')==='1')settings.finished();
return new ArrayBuffer(0);
}
const next=response.headers.get('X-Down-Cursor')||'',base=settings.cursor();
if(!/^[1-9]\d*$/.test(next)||BigInt(next)!==BigInt(base)+1n||BigInt(next)>18446744073709551615n)throw protocol('invalid downlink cursor');
const length=response.headers.get('Content-Length');let declared=null;
if(length!==null){if(!/^(0|[1-9]\d*)$/.test(length))throw protocol('invalid response length');declared=Number(length);if(!Number.isSafeInteger(declared)||declared===0||declared>settings.limit())throw protocol('response body overflow')}
if(transaction&&(transaction.base!==base||transaction.next!==next||(transaction.declared!==null&&declared!==null&&transaction.declared!==declared)))throw protocol('changed replay metadata');
if(!transaction){
const capacity=declared===null?settings.limit():declared,release=await settings.budget.acquire(capacity,signal);
if(!current(signal)){release();throw new Error('downlink retired')}
transaction={base,next,declared,bytes:new Uint8Array(capacity),retained:0,delivered:0,release};
}
const owner=transaction;
if(owner.declared===null&&declared!==null)owner.declared=declared;
await settings.ready(signal);
if(!current(signal)||transaction!==owner)throw new Error('downlink retired');
if(!response.body)throw protocol('missing response body');
const reader=response.body.getReader();let position=0,offset=0,frames=0,chunks=0,complete=false;
try{
for(;;){
let part;try{part=await reader.read()}catch(error){throw settings.failure('network','response stream interrupted')}
if(!current(signal)||transaction!==owner)throw new Error('downlink retired');
if(part.done)break;
const data=part.value;if(!(data instanceof Uint8Array)||++chunks>65536||data.byteLength>owner.bytes.byteLength-position)throw protocol('response body overflow');
const compared=Math.min(data.byteLength,Math.max(0,owner.retained-position));
for(let i=0;i<compared;i++)if(data[i]!==owner.bytes[position+i])throw protocol('changed replay prefix');
owner.bytes.set(data.subarray(compared),position+compared);position+=data.byteLength;owner.retained=Math.max(owner.retained,position);
while(position-offset>=8){
const view=new DataView(owner.bytes.buffer,offset),type=view.getUint8(0),id=(view.getUint8(1)<<16)|(view.getUint8(2)<<8)|view.getUint8(3),size=view.getUint32(4),end=offset+8+size;
if(!shape(type,id,size)||(settings.lane!==null&&id!==settings.lane)||end>owner.bytes.byteLength)throw protocol('invalid downlink frame');
if(end>position)break;if(++frames>4096)throw protocol('too many downlink frames');
if(type===4&&view.getUint32(8)===0)throw protocol('invalid WINDOW credit');
if(end>owner.delivered){
if(offset!==owner.delivered)throw protocol('invalid replay boundary');
const frame=owner.bytes.slice(offset,end).buffer;
settings.deliver(frame);owner.delivered=end;
}
offset=end;
}
}
if(!position||offset!==position||position<owner.retained||(owner.declared!==null&&position!==owner.declared))throw protocol('incomplete downlink batch');
settings.advance(next);complete=true;dispose();return new ArrayBuffer(0);
}finally{
if(!complete)try{const cancelled=reader.cancel();if(cancelled)cancelled.catch(()=>{})}catch(error){}
reader.releaseLock();
}
}
return {read,close(){closed=true;dispose()}};
}
globalThis.TelemtBridgeDownlink=Object.freeze({create,budget});
})();
+6 -5
View File
@@ -26,12 +26,13 @@ function create(settings){
if(status===204&&(path==='/api/v1/up'||path==='/api/v1/down'))return {limit:0,exact:true,reason:'protocol'};
return {limit:0,exact:true,reason:'http'};
}
async function send(path,frozenOptions,remainingBudget,maxAttempts){
async function send(path,frozenOptions,remainingBudget,maxAttempts,receiver){
let delay=250,attempt=0,lastReason='network';maxAttempts=maxAttempts||9;
const attempts=()=>typeof maxAttempts==='function'?maxAttempts():maxAttempts;
const initialBudget=remainingBudget?Math.min(settings.retryMs(),remainingBudget()):settings.retryMs();
const deadline=Date.now()+Math.max(0,initialBudget),external=frozenOptions.signal;
const attemptLimit=path==='/api/v1/down'?settings.longPollMs()+settings.requestMs():settings.requestMs();
while(attempt<maxAttempts){
while(attempt<attempts()){
if(settings.closed()||(external&&external.aborted))throw new Error('request aborted');
const remaining=Math.min(deadline-Date.now(),remainingBudget?remainingBudget():Infinity);if(remaining<=0)break;attempt++;
const controller=new AbortController(),abort=()=>controller.abort();let timedOut=false;
@@ -45,12 +46,12 @@ function create(settings){
lastReason='http';wait=retryAfterMs(fetched);settings.cancel(fetched);
}else{
const policy=responsePolicy(path,fetched.status);let body;
try{body=await settings.read(fetched,policy.limit,policy.exact,controller.signal)}
try{body=receiver&&(fetched.status===200||fetched.status===204)?await receiver(fetched,controller.signal):await settings.read(fetched,policy.limit,policy.exact,controller.signal)}
catch(error){
controller.abort();
if(external&&external.aborted)throw error;
if(timedOut)throw settings.failure('timeout','response deadline exceeded');
throw settings.failure(policy.reason,error&&error.message);
throw settings.failure(settings.reason(error,policy.reason),error&&error.message);
}
response={status:fetched.status,headers:fetched.headers,body};return response;
}
@@ -60,7 +61,7 @@ function create(settings){
if(timedOut)lastReason='timeout';
if(settings.reason(error,'')==='protocol')throw error;
}finally{clearTimeout(timer);if(external)external.removeEventListener('abort',abort)}
const after=Math.min(deadline-Date.now(),remainingBudget?remainingBudget():Infinity);if(attempt>=maxAttempts||after<=0)break;
const after=Math.min(deadline-Date.now(),remainingBudget?remainingBudget():Infinity);if(attempt>=attempts()||after<=0)break;
settings.retrying();
const backoff=wait||delay+Math.floor(Math.random()*Math.max(1,delay/4));
await pause(Math.min(backoff,after),external);delay=Math.min(delay*2,2000);
+42 -141
View File
@@ -19,7 +19,7 @@ let laneQueueLimit=Math.min(queueLimit,8388608),laneItemLimit=Math.min(queueItem
const fragment=location.hash,androidNonce=/^#android=([A-Za-z0-9_-]{43})$/.exec(fragment)?.[1]||'',recoveryPath=location.pathname+location.search;
history.replaceState(null,'',location.pathname);
let initialized=false,closed=false,port=null,sessionToken='',cleanupToken='',createStarted=false,socket=null,socketReady=false,carrier='';
let upSequence=1,downCursor='0',upRunning=false,upLease=null,pollController=null;
let upSequence=1,downCursor='0',upRunning=false,upLease=null,pollController=null,httpCarrier=null,upWindow=1;
let helloFrame=null,helloTimer=null,welcomeSent=false,carrierAttempt=1,carrierFailure='',carrierCommitted=false,terminalFailure='';
let negotiationStartedAt=0,carrierTimer=null,probeTimer=null,attemptController=null,attemptEpoch=1,candidateRunning=false,switching=false,currentAttempt=null;
let recoveryController=null,recoveryCommit=null,recoveryReplaced=false,lastSchedulerWall=Date.now(),lastSchedulerMonotonic=performance.now(),schedulerGapPending=0,schedulerTimer=null;
@@ -44,7 +44,7 @@ const {reserve,release,releasePending,frameBound,splitFrames,acceptNativeFrames,
function detachLease(lease){if(lease.lane){if(lease.lane.upLease===lease)lease.lane.upLease=null}else if(upLease===lease)upLease=null}
function settleBatch(lease){if(!buffers.settleBatch(lease))return false;detachLease(lease);return true}
function cancelBatch(lease){if(!lease||lease.settled)return;buffers.cancelBatch(lease);detachLease(lease)}
const attemptHeaders=(attempt,failure)=>negotiationEnabled?Object.assign({'X-Carrier-Capabilities':carrierCapabilities,'X-Carrier-Attempt':String(attempt)},failure?{'X-Carrier-Failure':failure}:{}):{};
const attemptHeaders=(attempt,failure)=>Object.assign({'X-Telemt-Up-Window':'4'},negotiationEnabled?Object.assign({'X-Carrier-Capabilities':carrierCapabilities,'X-Carrier-Attempt':String(attempt)},failure?{'X-Carrier-Failure':failure}:{}):{});
function finishOldRecovery(){
recoveryReplaced=false;resetScheduler();status('connected');
while(recoveryPending.length&&!closed){const data=recoveryPending.shift();release(data.byteLength,1,null);queueCarrier(data)}
@@ -58,6 +58,7 @@ function resolveRecoveryCommit(){
commit.signal.removeEventListener('abort',commit.abort);commit.resolve();
}
function retireCarrier(policy){
stopHttp(false);upWindow=1;
recoveryReplaced=true;attemptEpoch++;if(carrierTimer)clearTimeout(carrierTimer);carrierTimer=null;clearProbeTimer();
if(attemptController)attemptController.abort();attemptController=null;if(pollController)pollController.abort();pollController=null;
if(socket){const previous=socket;socket=null;previous.close()}socketReady=false;cancelBatch(upLease);releasePending(upPending,null);
@@ -118,9 +119,10 @@ function fail(reason){
}
function knownCarrier(value){return value==='https'||value==='https-lanes'||value==='websocket'||value==='websocket-lanes'}
function sessionEcho(response,expectedAttempt,states,exactAttempt){
const selected=response.headers.get('X-Carrier-Mode')||'',echo=response.headers.get('X-Carrier-Attempt')||'';
const selected=response.headers.get('X-Carrier-Mode')||'',echo=response.headers.get('X-Carrier-Attempt')||'',windowHeader=response.headers.get('X-Telemt-Up-Window');
if(windowHeader!==null&&!/^[1-4]$/.test(windowHeader))throw new Error('invalid conveyor window');const upWindow=windowHeader===null?1:Number(windowHeader);
if(!knownCarrier(selected))throw new Error('invalid carrier mode');
if(!negotiationEnabled){if(echo!=='')throw new Error('unexpected carrier attempt');return {selected,state:''}}
if(!negotiationEnabled){if(echo!=='')throw new Error('unexpected carrier attempt');return {selected,state:'',upWindow}}
const count=response.headers.get('X-Carrier-Candidate-Count')||'',deadline=response.headers.get('X-Carrier-Deadline')||'',state=response.headers.get('X-Carrier-State')||'';
if(!/^[1-4]$/.test(count)||!/^[1-9]\d*$/.test(deadline)||!states.includes(state))throw new Error('invalid carrier state');
const echoedAttempt=Number(echo),parsedCount=Number(count),parsedDeadline=Number(deadline);
@@ -129,7 +131,7 @@ function sessionEcho(response,expectedAttempt,states,exactAttempt){
if(!negotiatedFrozen){negotiatedCandidateCount=parsedCount;negotiatedFinalDeadline=parsedDeadline;negotiatedFrozen=true}
else if(parsedCount!==negotiatedCandidateCount||parsedDeadline!==negotiatedFinalDeadline)throw new Error('changed carrier bounds');
if(echoedAttempt>negotiatedCandidateCount)throw new Error('carrier attempt exceeds candidates');
return {selected,state};
return {selected,state,upWindow};
}
function armCarrierDeadline(epoch){
if(!negotiationStartedAt||epoch!==attemptEpoch)return;
@@ -140,6 +142,7 @@ function armCarrierDeadline(epoch){
}
function clearProbeTimer(){if(probeTimer){clearTimeout(probeTimer.timer);probeTimer=null}}
function resetCandidate(){
stopHttp(true);upWindow=1;
clearProbeTimer();
if(pollController)pollController.abort();pollController=null;
if(socket){const previous=socket;socket=null;previous.close()}socketReady=false;
@@ -198,8 +201,8 @@ function startCandidate(probe,epoch){
else advanceCarrier('protocol',epoch);
}
function maybeStartCandidate(){
if(closed||carrierCommitted||!sessionToken||candidateRunning)return;const epoch=attemptEpoch;
let probe;try{probe=findProbe(probeCoalesceMs>0)}catch(error){fail('protocol');return}if(!probe)return;
if(closed||carrierCommitted||!sessionToken)return;if(candidateRunning){try{flushHttpPending()}catch(error){fail(failureReason(error,'protocol'))}return}const epoch=attemptEpoch;
let probe;try{probe=findProbe(true)}catch(error){fail('protocol');return}if(!probe)return;
if(!probeCoalesceMs||probe.hasData){startCandidate(probe,epoch);return}
if(probeTimer)return;const owner={epoch,timer:null};
owner.timer=setTimeout(()=>{if(probeTimer!==owner||closed||owner.epoch!==attemptEpoch)return;probeTimer=null;let current;try{current=findProbe(false)}catch(error){fail('protocol');return}startCandidate(current,owner.epoch)},probeCoalesceMs);
@@ -219,74 +222,58 @@ async function createSession(epoch){
const token=response.headers.get('X-Session-Token')||'',cursor=response.headers.get('X-Down-Cursor')||'';
if(!token||cursor!=='0'){advanceCarrier('protocol',epoch);return}
const welcome=response.body;if(closed||epoch!==attemptEpoch)return;
carrier=selected;sessionToken=token;cleanupToken=token;downCursor=cursor;
carrier=selected;sessionToken=token;cleanupToken=token;downCursor=cursor;upWindow=echo.upWindow;
if(!welcomeSent){welcomeSent=true;port.postMessage(welcome,[welcome]);status('connecting')}
if(carrier==='websocket')openCandidateSocket(null,null,epoch);
maybeStartCandidate();
}catch(error){if(closed||epoch!==attemptEpoch)return;advanceCarrier(failureReason(error,'network'),epoch)}
}
async function probeHttp(probe,laneID,epoch){
try{
const headers={'X-Up-Seq':'1'},token=sessionToken,controller=attemptController,body=probe.data;if(laneID!==null)headers['X-Lane-ID']=String(laneID);
const response=await request('/api/v1/up',options(carrierMethod,token,body,headers,controller.signal));
if(closed||epoch!==attemptEpoch)return
if(response.status!==204){advanceCarrier('http',epoch);return}
if(response.headers.get('X-Up-Ack')!=='1'){advanceCarrier('protocol',epoch);return}
if(laneID===null)upSequence=2;else ensureLane(laneID).sequence=2;
commitCarrier(probe,epoch);
}catch(error){if(!closed&&epoch===attemptEpoch)advanceCarrier(failureReason(error,'network'),epoch)}
function stopHttp(restore){
if(!httpCarrier)return;const saved=httpCarrier.close(restore);httpCarrier=null;
for(const data of saved){if(!reserve(data,null)){fail('capacity');return}pending.push(data)}
}
function flushHttpPending(){
while(httpCarrier&&pending.length&&!closed){const data=pending.shift();release(data.byteLength,1,null);httpCarrier.enqueue(data,true)}
if(httpCarrier&&!closed)httpCarrier.flush();
}
function probeHttp(probe,laneID,epoch){
const token=sessionToken;
httpCarrier=globalThis.TelemtBridgeConveyor.create({
token,window:upWindow,method:carrierMethod,lanes:carrier==='https-lanes',buffers,options,request,
queueLimit:()=>queueLimit,batchLimit:()=>batchLimit,committed:()=>carrierCommitted,failure,cancel:responseBody.cancel,
alive:lane=>!closed&&epoch===attemptEpoch&&token===sessionToken&&(!lane||lanes.get(lane.id)===lane),
commit:()=>commitCarrier(null,epoch),recover:recoverTransport,connected:()=>status('connected'),
traffic:(up,down)=>port.postMessage({t:'traffic',up,down}),
deliver:data=>{observeServerFrames(data);port.postMessage({t:'traffic',up:0,down:data.byteLength});port.postMessage(data,[data])},
lane:(id,type)=>{
let lane=lanes.get(id);if(!lane&&(type===2||type===3||type===4))return null;
if(!lane&&closedLanes.has(id))throw failure('protocol','closed lane reused');
if(!lane&&id!==0&&type!==1)throw failure('protocol','lane did not begin with OPEN');
return lane||ensureLane(id);
},
finish:finishLane,failed:error=>{if(closed||epoch!==attemptEpoch)return;if(carrierCommitted)fail(failureReason(error,'network'));else advanceCarrier(failureReason(error,'network'),epoch)}
});
try{flushHttpPending()}catch(error){fail(failureReason(error,'protocol'))}
}
function commitCarrier(probe,epoch){
if(closed||carrierCommitted||epoch!==attemptEpoch)return;
if(switching){fail('protocol');return}
clearProbeTimer();try{consumeProbe(probe)}catch(error){fail('protocol');return}
clearProbeTimer();try{if(probe)consumeProbe(probe)}catch(error){fail('protocol');return}
carrierCommitted=true;candidateRunning=false;if(carrierTimer)clearTimeout(carrierTimer);carrierTimer=null;
resetScheduler();
attemptController=null;currentAttempt=null;
status('connected');
if(carrier==='https')poll();
else if(carrier==='https-lanes'){const lane=lanes.get(probe.id);if(lane&&!lane.polling)pollLane(lane)}
while(pending.length&&!closed){const data=pending.shift();release(data.byteLength,1,null);queueCarrier(data)}
resolveRecoveryCommit();
}
function queueCarrier(data){
if(closed)return;
try{
if(carrier==='https')queueUp(data);
if(carrier==='https'||carrier==='https-lanes')httpCarrier.enqueue(data);
else if(carrier==='websocket')queueSocket(data);
else for(const value of splitFrames(data)){if(closed)break;queueLane(value)}
}catch(error){fail('protocol')}
}
function queueUp(data){if(!reserve(data,null)){fail('capacity');return}upPending.push(data);runUp()}
async function runUp(){
if(upRunning)return;upRunning=true;const ownerEpoch=attemptEpoch;let lease=null;
try{
while(!closed&&sessionToken&&upPending.length){
lease=takeBatch(upPending,null);upLease=lease;lease.controller=new AbortController();const sequence=String(upSequence),token=sessionToken;
for(;;){
try{
const response=await request('/api/v1/up',options(carrierMethod,token,lease.body,{'X-Up-Seq':sequence},lease.controller.signal),null,1);
if(response.status!==204)throw failure('http','uplink rejected');
if(response.headers.get('X-Up-Ack')!==sequence)throw failure('protocol','uplink acknowledgement rejected');
break;
}catch(error){
let replayed=false;
const recovered=await recoverTransport(error,async(signal,remaining)=>{
const response=await request('/api/v1/up',options(carrierMethod,token,lease.body,{'X-Up-Seq':sequence},signal),remaining,2);
if(response.status!==204)throw failure('http','uplink replay rejected');
if(response.headers.get('X-Up-Ack')!==sequence)throw failure('protocol','uplink replay acknowledgement rejected');
replayed=true;
});
if(!recovered||closed||lease.cancelled||sessionToken!==token)return;
if(!replayed)continue;
break;
}
}
if(!settleBatch(lease))return;port.postMessage({t:'traffic',up:lease.total,down:0});upSequence++;lease=null;
}
}catch(error){if(!closed&&!(lease&&lease.cancelled))fail(failureReason(error,'network'))}
finally{if(ownerEpoch===attemptEpoch){upRunning=false;if(!closed&&sessionToken&&upPending.length)runUp()}}
}
function sendCandidateSocket(next){
const state=next.telemt;if(!state||state.sent||next.readyState!==WebSocket.OPEN||!state.probe)return;
let probe=state.probe;
@@ -340,30 +327,6 @@ async function runSocketUp(){
}catch(error){if(!closed&&!(lease&&lease.cancelled))recoverTransport(error,null)}
finally{if(ownerEpoch===attemptEpoch){upRunning=false;if(!closed&&socketReady&&upPending.length)runSocketUp()}}
}
async function poll(){
while(!closed&&sessionToken){
const token=sessionToken,cursor=downCursor;
try{
pollController=new AbortController();
const response=await request('/api/v1/down',options(carrierMethod,token,null,{'X-Down-Cursor':cursor},pollController.signal),null,1);
if(closed||sessionToken!==token)return;
if(response.status===204){status('connected');continue}
if(response.status!==200)throw failure('http','downlink rejected');
const next=response.headers.get('X-Down-Cursor')||'',data=response.body;
if(!next||!data.byteLength)throw failure('protocol','invalid downlink response');
if(closed)return;
observeServerFrames(data);port.postMessage({t:'traffic',up:0,down:data.byteLength});port.postMessage(data,[data]);downCursor=next;status('connected');
}catch(error){
if(closed)return;
const recovered=await recoverTransport(error,async(signal,remaining)=>{
const response=await request('/api/v1/down',options(carrierMethod,token,null,{'X-Down-Cursor':cursor},signal),remaining,2);
if(response.status===204)return;
if(response.status!==200||!response.body.byteLength||!response.headers.get('X-Down-Cursor'))throw failure('http','downlink replay rejected');
});
if(!recovered||closed||sessionToken!==token)return;
}
}
}
function ensureLane(id){
let lane=lanes.get(id);
if(!lane){lane={id,sequence:1,cursor:'0',pending:[],bytes:0,items:0,running:false,upLease:null,polling:false,controller:null,socket:null,ready:false,remoteClosed:false};lanes.set(id,lane)}
@@ -375,7 +338,7 @@ function rememberLaneClosed(id){
closedLanes.add(id);closedLaneOrder.push(id);
}
function finishLane(lane,notifyClient){
if(lanes.get(lane.id)!==lane)return;
if(lanes.get(lane.id)!==lane)return;if(httpCarrier)httpCarrier.finish(lane);
if(lane.controller)lane.controller.abort();lane.controller=null;cancelBatch(lane.upLease);
if(lane.socket&&lane.socket.readyState<WebSocket.CLOSING)lane.socket.close();
releasePending(lane.pending,lane);lanes.delete(lane.id);rememberLaneClosed(lane.id);
@@ -389,7 +352,7 @@ function queueLane(value){
lane=lane||ensureLane(value.id);
if(!reserve(value.data,lane)){fail('capacity');return}
lane.pending.push(value.data);
if(carrier==='websocket-lanes'){openLaneSocket(lane);runLaneSocketUp(lane)}else runLaneUp(lane);
openLaneSocket(lane);runLaneSocketUp(lane);
}
function openLaneSocket(lane){
if(lane.socket||closed)return;lane.socket=new WebSocket(socketURL(),'tproxy-lane-v1.'+sessionToken+'.'+String(lane.id));lane.socket.binaryType='arraybuffer';
@@ -416,74 +379,12 @@ async function runLaneSocketUp(lane){
}catch(error){if(!closed&&lanes.get(lane.id)===lane&&!(lease&&lease.cancelled))finishLane(lane,true)}
finally{lane.running=false;if(!closed&&lanes.get(lane.id)===lane&&lane.ready&&lane.pending.length)runLaneSocketUp(lane)}
}
async function runLaneUp(lane){
if(lane.running)return;lane.running=true;let lease=null;
try{
while(!closed&&sessionToken&&lane.pending.length){
lease=takeBatch(lane.pending,lane);lane.upLease=lease;lease.controller=new AbortController();
const sequence=String(lane.sequence),laneID=String(lane.id),token=sessionToken;
for(;;){
try{
const response=await request('/api/v1/up',options(carrierMethod,token,lease.body,{'X-Up-Seq':sequence,'X-Lane-ID':laneID},lease.controller.signal),null,1);
if(response.status!==204)throw failure('http','lane uplink rejected');
if(response.headers.get('X-Up-Ack')!==sequence)throw failure('protocol','lane uplink acknowledgement rejected');
break;
}catch(error){
let replayed=false;
const recovered=await recoverTransport(error,async(signal,remaining)=>{
const response=await request('/api/v1/up',options(carrierMethod,token,lease.body,{'X-Up-Seq':sequence,'X-Lane-ID':laneID},signal),remaining,2);
if(response.status!==204)throw failure('http','lane uplink replay rejected');
if(response.headers.get('X-Up-Ack')!==sequence)throw failure('protocol','lane uplink replay acknowledgement rejected');
replayed=true;
});
if(!recovered||closed||lease.cancelled||sessionToken!==token||lanes.get(lane.id)!==lane)return;
if(!replayed)continue;
break;
}
}
if(!settleBatch(lease))return;port.postMessage({t:'traffic',up:lease.total,down:0});lane.sequence++;lease=null;
if(!lane.polling)pollLane(lane);
}
}catch(error){if(!closed&&lanes.get(lane.id)===lane&&!(lease&&lease.cancelled))fail(failureReason(error,'network'))}
finally{lane.running=false;if(!closed&&lanes.get(lane.id)===lane&&sessionToken&&lane.pending.length)runLaneUp(lane)}
}
async function pollLane(lane){
if(!lane||lane.polling)return;lane.polling=true;let restart=false,failedToken='',failedCursor='',failedLaneID='';
try{
while(!closed&&sessionToken&&lanes.get(lane.id)===lane){
const controller=new AbortController(),laneID=String(lane.id),token=sessionToken,cursor=lane.cursor;lane.controller=controller;
failedToken=token;failedCursor=cursor;failedLaneID=laneID;
const response=await request('/api/v1/down',options(carrierMethod,token,null,{'X-Down-Cursor':cursor,'X-Lane-ID':laneID},controller.signal),null,1);
if(closed||sessionToken!==token||lanes.get(lane.id)!==lane)return;
if(response.status===204){
if(response.headers.get('X-Lane-Closed')==='1'){finishLane(lane,false);return}
status('connected');continue;
}
if(response.status!==200)throw failure('http','lane downlink rejected');
const next=response.headers.get('X-Down-Cursor')||'',data=response.body;
if(!next||!data.byteLength)throw failure('protocol','invalid lane downlink response');
for(const value of splitFrames(data))if(value.id!==lane.id)throw new Error('cross-lane frame');
if(closed)return;
observeServerFrames(data);port.postMessage({t:'traffic',up:0,down:data.byteLength});port.postMessage(data,[data]);lane.cursor=next;status('connected');
}
}catch(error){
if(!closed&&lanes.get(lane.id)===lane){
const recovered=await recoverTransport(error,async(signal,remaining)=>{
const response=await request('/api/v1/down',options(carrierMethod,failedToken,null,{'X-Down-Cursor':failedCursor,'X-Lane-ID':failedLaneID},signal),remaining,2);
if(response.status===204)return;
if(response.status!==200||!response.body.byteLength||!response.headers.get('X-Down-Cursor'))throw failure('http','lane downlink replay rejected');
});
restart=recovered&&!closed&&sessionToken===failedToken&&lanes.get(lane.id)===lane;
}
}
finally{lane.polling=false;lane.controller=null;if(restart)pollLane(lane)}
}
function deleteSession(){
const token=cleanupToken||sessionToken,headers=canonicalFailures.includes(terminalFailure)?{'X-Carrier-Failure':terminalFailure}:null;
if(token)fetch(relayBase+'/api/v1/session',options('DELETE',token,null,headers,undefined,true)).catch(()=>{});
}
function close(notifyServer){
if(closed)return;closed=true;if(recoveryController)recoveryController.cancel();rejectRecoveryCommit(failure('network','bridge closed'));if(helloTimer)clearTimeout(helloTimer);helloTimer=null;if(carrierTimer)clearTimeout(carrierTimer);clearProbeTimer();if(schedulerTimer)clearTimeout(schedulerTimer);schedulerTimer=null;if(attemptController)attemptController.abort();if(pollController)pollController.abort();
if(closed)return;closed=true;stopHttp(false);if(recoveryController)recoveryController.cancel();rejectRecoveryCommit(failure('network','bridge closed'));if(helloTimer)clearTimeout(helloTimer);helloTimer=null;if(carrierTimer)clearTimeout(carrierTimer);clearProbeTimer();if(schedulerTimer)clearTimeout(schedulerTimer);schedulerTimer=null;if(attemptController)attemptController.abort();if(pollController)pollController.abort();
if(socket)socket.close();cancelBatch(upLease);releasePending(upPending,null);
for(const lane of lanes.values()){
if(lane.controller)lane.controller.abort();cancelBatch(lane.upLease);releasePending(lane.pending,lane);if(lane.socket)lane.socket.close();
+8 -16
View File
@@ -31,20 +31,8 @@ fn carrier_method_is_page_owned_and_used_by_every_https_request() {
.contains(&format!("const carrierMethod='{}';", method.as_str()))
);
assert_eq!(page.body.matches("carrierMethod=").count(), 1);
assert_eq!(page.body.matches("options(carrierMethod,").count(), 9);
let requests: Vec<_> = page
.body
.lines()
.filter(|line| {
line.contains("request('/api/v1/up'") || line.contains("request('/api/v1/down'")
})
.collect();
assert_eq!(requests.len(), 9);
assert!(
requests
.iter()
.all(|line| line.contains("options(carrierMethod,"))
);
// Execute the rendered POST/PUT page, including retries, instead of counting call sites.
behavior_tests::run(&page);
assert_eq!(page.body.matches("options('POST',bootstrap,").count(), 2);
assert!(
page.body
@@ -68,6 +56,9 @@ fn carrier_method_is_page_owned_and_used_by_every_https_request() {
}
}
#[path = "behavior_tests.rs"]
mod behavior_tests;
fn render_page(bootstrap: &str, candidate_count: usize) -> BridgePage {
render(
"proxy.example.com",
@@ -289,8 +280,9 @@ fn disabled_negotiation_does_not_arm_a_carrier_deadline() {
fn retry_and_attempt_state_are_frozen_before_fetch() {
let page = render_page("EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", 4);
assert!(
page.body
.contains("async function send(path,frozenOptions,remainingBudget,maxAttempts)")
page.body.contains(
"async function send(path,frozenOptions,remainingBudget,maxAttempts,receiver)"
)
);
assert!(!page.body.contains("makeOptions"));
assert!(page.body.contains(