Files
telemt/src/web/bridge/conveyor.js
T
2026-10-03 20:29:18 +03:00

107 lines
6.4 KiB
JavaScript

(()=>{'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});
})();