69 lines
3.9 KiB
JavaScript
69 lines
3.9 KiB
JavaScript
'use strict';
|
|
const {EventEmitter}=require('node:events');
|
|
const {TextDecoder}=require('node:util');
|
|
const SMALL=65536,LARGE=2097152;
|
|
// Transport only: the native host and parent must authenticate their pipe peer
|
|
// before constructing this channel. Framing does not grant policy authority.
|
|
function createFramedChannel({readable,writable,inboundFirstLimit=SMALL,outboundFirstLimit=SMALL}={}){
|
|
if(!readable||!writable||typeof readable.on!=='function'||typeof writable.write!=='function'||![SMALL,LARGE].includes(inboundFirstLimit)||![SMALL,LARGE].includes(outboundFirstLimit))throw Error('broker-channel-invalid');
|
|
const channel=new EventEmitter(),decoder=new TextDecoder('utf-8',{fatal:true});
|
|
let connected=true,firstRead=true,firstWrite=true,header=Buffer.alloc(4),headerBytes=0,payload,payloadBytes=0,pending=0;
|
|
const outstanding=new Set();
|
|
Object.defineProperty(channel,'connected',{get:()=>connected});
|
|
function disconnect(){
|
|
if(!connected)return;connected=false;header.fill(0);payload?.fill(0);payload=undefined;
|
|
readable.off('data',data);readable.off('end',disconnect);readable.off('close',disconnect);
|
|
// Keep bounded error listeners until destroyed streams settle.
|
|
readable.destroy?.();if(writable!==readable)writable.destroy?.();
|
|
for(const record of outstanding)record.notify(true);
|
|
channel.emit('disconnect');
|
|
}
|
|
function data(chunk){
|
|
if(!connected)return;
|
|
try{
|
|
if(!Buffer.isBuffer(chunk))throw Error();
|
|
let offset=0;
|
|
while(offset<chunk.length&&connected){
|
|
if(!payload){
|
|
const take=Math.min(4-headerBytes,chunk.length-offset);chunk.copy(header,headerBytes,offset,offset+take);headerBytes+=take;offset+=take;
|
|
if(headerBytes!==4)continue;
|
|
const length=header.readUInt32LE(0),limit=firstRead?inboundFirstLimit:SMALL;
|
|
if(length<1||length>limit)throw Error();
|
|
payload=Buffer.alloc(length);payloadBytes=0;headerBytes=0;header.fill(0);
|
|
}
|
|
const take=Math.min(payload.length-payloadBytes,chunk.length-offset);chunk.copy(payload,payloadBytes,offset,offset+take);payloadBytes+=take;offset+=take;
|
|
if(payloadBytes===payload.length){
|
|
const completed=payload;payload=undefined;payloadBytes=0;firstRead=false;
|
|
let message;try{message=JSON.parse(decoder.decode(completed));}finally{completed.fill(0);}
|
|
if(!message||typeof message!=='object'||Array.isArray(message))throw Error();
|
|
channel.emit('message',message);
|
|
}
|
|
}
|
|
}catch{disconnect();}
|
|
}
|
|
channel.send=(message,callback=()=>{})=>{
|
|
if(typeof callback!=='function')throw Error('broker-channel-invalid');
|
|
const notifyFailure=()=>{disconnect();try{callback(Error('broker-channel-unavailable'));}catch{}};
|
|
if(!connected||pending>=8){notifyFailure();return;}
|
|
let frame;
|
|
try{
|
|
if(!message||typeof message!=='object'||Array.isArray(message))throw Error();
|
|
const text=JSON.stringify(message),bytes=Buffer.byteLength(text),limit=firstWrite?outboundFirstLimit:SMALL;
|
|
if(bytes<1||bytes>limit)throw Error();
|
|
frame=Buffer.alloc(bytes+4);frame.writeUInt32LE(bytes);frame.write(text,4);firstWrite=false;pending++;
|
|
}catch{notifyFailure();return;}
|
|
let settled=false,notified=false;
|
|
const record={notify(error){if(notified)return;notified=true;try{callback(error?Error('broker-channel-unavailable'):undefined);}catch{disconnect();}}};
|
|
outstanding.add(record);
|
|
// Completion of the underlying write owns buffer release. Disconnect notifies
|
|
// failure immediately, but cannot claim that an outstanding write has settled.
|
|
const done=error=>{if(settled)return;settled=true;pending--;outstanding.delete(record);frame.fill(0);if(error)disconnect();record.notify(error||!connected);};
|
|
try{writable.write(frame,done);}catch{done(Error());}
|
|
};
|
|
channel.disconnect=disconnect;
|
|
readable.on('data',data);readable.once('end',disconnect);readable.once('close',disconnect);readable.on('error',disconnect);
|
|
if(writable!==readable){writable.once('close',disconnect);writable.on('error',disconnect);}
|
|
return channel;
|
|
}
|
|
module.exports={createFramedChannel};
|