mirror of
https://github.com/telemt/telemt.git
synced 2026-10-27 02:57:37 +03:00
107 lines
6.4 KiB
JavaScript
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});
|
|
})();
|