@phnq/message
Version:
Asynchronous, incremental messaging client and server
2 lines (1 loc) • 13.5 kB
JavaScript
import{createLogger as m}from"@phnq/log";import{AsyncQueue as u}from"@phnq/streams";import T from"browser-process-hrtime";import{v4 as g}from"uuid";class H extends Error{isAnomaly=!0;info;constructor(i,n){super(i);Object.setPrototypeOf(this,H.prototype),this.info=n}}var J;((o)=>{o.Request="request";o.Response="response";o.Multi="multi";o.End="end";o.Error="error";o.Anomaly="anomaly"})(J||={});import{createLogger as M}from"@phnq/log";import b from"object-hash";import{v4 as h}from"uuid";var l=M("sign"),N=(i,n,R)=>b({m:{t:i.t,c:i.c,s:i.s,p:JSON.stringify(i.p),u:n},salt:R}),Q=(i,n)=>{let R=h().replace(/-/g,"");return{...i,z:[R,N(i,R,n)].join(":")}},_=(i,n)=>{let{z:R,...t}=i,[w,r]=R?R.split(":"):[],o=N(t,w??"",n);if(r!==o)throw l.error("Failed to verify message: ",o,i),Error("Failed to verify message");return i};var U=m("MessageConnection"),a=function*(){let i=0;while(!0)yield++i}(),E=(i)=>{switch(i.t){case"anomaly":{let n=i;throw new H(n.p.message,n.p.info)}case"error":throw Error(i.p.message)}},s;((R)=>{R.Requester="requester";R.Responder="responder"})(s||={});var e=30000;class L{responseTimeout=e;connId=g();transport;responseQueues=new Map;signSalt;marshalPayload;unmarshalPayload;_attributes=new Map;receiveHandler;onConversation;constructor(i,{signSalt:n,marshalPayload:R,unmarshalPayload:t}={}){this.transport=i,this.signSalt=n,this.marshalPayload=R||((w)=>w),this.unmarshalPayload=t||((w)=>w),i.onReceive(async(w)=>{let r;if(this.signSalt)try{_(w,this.signSalt)}catch(O){r={c:w.c,s:w.s,t:"error",p:{message:O.message??"Failed to verify message",requestPayload:w.p}}}let o=this.unmarshalMessage(r||w);if(o.t==="request"){await this.handleRequest(o);return}let A=this.responseQueues.get(o.c);if(A)switch(o.t){case"response":case"anomaly":case"error":case"end":A.enqueue(o),A.flush();break;case"multi":A.enqueue(o);break}})}get onReceive(){return this.receiveHandler}set onReceive(i){this.receiveHandler=i}get id(){return this.connId}get attributes(){return Object.fromEntries(this._attributes)}getAttribute(i){return this._attributes.get(i)}setAttribute(i,n){this._attributes.set(i,n)}deleteAttribute(i){this._attributes.delete(i)}async send(i){await this.requestOne(i,!1)}async requestOne(i,n=!0){let R=await this.request(i,n);if(typeof R==="object"&&R[Symbol.asyncIterator]){let t=[];for await(let w of R)t.push(w);if(t.length>1)U.warn("requestOne: multiple responses were returned -- all but the first were discarded");if(!t[0])throw Error("requestOne: no responses were returned");return t[0]}else return R}async requestMulti(i){let n=await this.request(i);if(typeof n==="object"&&n[Symbol.asyncIterator])return n;else return async function*(){yield n}()}async request(i,n=!0){return this.doRequest(i,n)}marshalMessage(i){switch(i.t){case"request":return{...i,p:this.marshalPayload(i.p)};case"response":case"multi":return{...i,p:this.marshalPayload(i.p)}}return i}unmarshalMessage(i){switch(i.t){case"request":return{...i,p:this.unmarshalPayload(i.p)};case"response":case"multi":return{...i,p:this.unmarshalPayload(i.p)}}return i}signMessage(i){if(this.signSalt)return Q(i,this.signSalt);return i}async doRequest(i,n){let R=a.next().value,t=this.responseQueues,w=this.id,r=this.signMessage({t:"request",c:R,p:i,s:w}),o={perspective:"requester",request:r,responses:[]},A=T(),O=new u;if(n)O.maxWaitTime=this.responseTimeout,t.set(R,O);if(await this.transport.send(r),n){let C=(await O.iterator().next()).value;o.responses.push({message:C,time:T(A)});let G=this.onConversation;if(C.t==="multi")return async function*(){yield C.p;try{for await(let x of O.iterator())if(x.s===C.s){if(o.responses.push({message:x,time:T(A)}),E(x),x.t==="multi")yield x.p}else U.warn("Received responses from multiple sources for request -- keeping the first, ignoring the rest: %s",JSON.stringify(i));if(G)G(o)}finally{t.delete(R)}}();else{if(t.delete(R),G)G(o);return E(C),C.p}}}async handleRequest(i){let n=this.id,R={perspective:"responder",request:i,responses:[]},t=T(),w=i.p,r=(o)=>{let A=this.signMessage(this.marshalMessage(o));this.transport.send(A).then(()=>{R.responses.push({message:A,time:T(t)})}).catch((O)=>{U.error("Failed to send response message: %s",O)})};if(!this.receiveHandler)throw Error("No receive handler set.");try{let o=await this.receiveHandler(w);if(typeof o==="object"&&o[Symbol.asyncIterator]){for await(let A of o)r({p:A,c:i.c,s:n,t:"multi"});r({c:i.c,s:n,t:"end",p:"END"})}else if(o)r({p:o,c:i.c,s:n,t:"response"})}catch(o){if(o instanceof H){let A={p:{message:o.message,info:o.info,requestPayload:w},c:i.c,s:n,t:"anomaly"};r(A)}else{let O={p:{message:o.message||String(o),requestPayload:w},c:i.c,s:n,t:"error"};r(O)}}finally{if(this.onConversation)this.onConversation(R)}}}var V=L;import"ws";import D from"isomorphic-ws";var W=(i)=>{if(Array.isArray(i))return i.map(W);if(i instanceof Date)return`${i.toISOString()}@@`;if(i&&typeof i==="object")return Object.fromEntries(Object.entries(i).map(([n,R])=>[n,W(R)]));return i},ii=/^(.+)@@$/,f=(i)=>{if(Array.isArray(i))return i.map(f);let n=typeof i==="string"?ii.exec(i):void 0;if(n?.[1])return new Date(n[1]);if(i&&typeof i==="object")return Object.fromEntries(Object.entries(i).map(([R,t])=>[R,f(t)]));return i},X=(i)=>JSON.stringify(W(i)),v=(i)=>f(JSON.parse(i));class P{socket;constructor(i){this.socket=i}async send(i){this.socket.send(X(i))}onReceive(i){this.socket.addListener("message",(n)=>{i(v(n.toString()))})}async close(){return new Promise((i)=>{this.socket.addListener("close",i),this.socket.close()})}}class B{onClose;url;socket;onReceiveFn;constructor(i){this.url=i}async send(i){if(await this.connect(),this.socket)this.socket.send(X(i))}onReceive(i){this.onReceiveFn=i}async close(){return new Promise((i)=>{if(this.socket)this.socket.addEventListener("close",()=>i()),this.socket.close(1000);else i()})}isOpen(){return this.socket!==void 0&&this.socket.readyState===D.OPEN}async connect(){if(this.socket&&this.socket.readyState===D.OPEN)return;else if(this.socket&&this.socket.readyState===D.CONNECTING)return new Promise((n)=>{this.socket?.addEventListener("open",()=>n())});let i;if(await new Promise((n,R)=>{try{this.socket=new D(this.url),this.socket.addEventListener("message",(t)=>{if(this.onReceiveFn)this.onReceiveFn(v(t.data.toString()))}),this.socket.addEventListener("close",(t)=>{if(i=t.reason,this.onClose)this.onClose();this.socket=void 0}),this.socket.addEventListener("open",()=>{n()}),this.socket.addEventListener("error",(t)=>{let w=`Socket error (${this.url}): ${t?.error?.message||"unknown error"}`;R(Error(w))})}catch(t){let w=`Error creating WebSocket (${this.url}): ${t instanceof Error?t.message:String(t)}`;R(Error(w))}}),this.socket&&this.socket.readyState===D.CLOSING)return new Promise((n,R)=>{if(this.socket)this.socket.addEventListener("close",(t)=>{R(Error(`Socket closed by server (${t.reason})`))})});else if(!this.socket||this.socket.readyState===D.CLOSED)throw Error(`Socket closed by server (${i??"unknown reason"})`)}}var F=P;var c=new Map;class K extends V{static create(i){let n=c.get(i);if(!n)n=new K(i),c.set(i,n);return n}onCloseHandlers=new Set;receiveHandlers=new Set;constructor(i){super(new B(i));this.transport.onClose=()=>{for(let n of this.onCloseHandlers)n()},super.onReceive=async(n)=>{for(let R of this.receiveHandlers)await R(n);return}}set onReceive(i){throw Error("onReceive is not supported on WebSocketMessageClient. Use addReceiveHandler instead.")}addReceiveHandler(i){this.receiveHandlers.add(i)}isOpen(){return this.transport.isOpen()}async close(){await this.transport.close()}set onClose(i){this.onCloseHandlers.add(i)}}var ni=K;import Ri from"node:assert";import ti from"isomorphic-ws";class p{wss;onConnect=async()=>{return};onDisconnect=async()=>{return};onReceive=async()=>{throw Error("WebSocketMessageServer.onReceive not set")};connectionsById=new Map;constructor({httpServer:i,path:n,paths:R}){if(n&&R)throw Error("Cannot specify both 'path' and 'paths'");this.wss=new ti.Server({server:i}),this.start(n?[n]:R??["/"])}getConnection(i){return this.connectionsById.get(i)}get connections(){return[...this.connectionsById.values()]}async close(){for(let i of this.connections)await i.transport.close();await new Promise((i,n)=>{try{this.wss.close(i)}catch(R){n(R)}})}start(i){this.wss.on("connection",async(n,R)=>{if(Ri(R.url,"WebSocket connection request must have a URL"),!i.includes(R.url)){n.close(1008,`unsupported path: ${R.url}`);return}let t=new V(new F(n));this.connectionsById.set(t.id,t),t.onReceive=(w)=>this.onReceive(t,w),n.addListener("close",async()=>{this.connectionsById.delete(t.id),await this.onDisconnect(t)}),await this.onConnect(t,R)})}}var oi=p;class I{connectedTransport;constructor(i){this.connectedTransport=i||new I(this)}getConnectedTransport(){return this.connectedTransport}async send(i){this.connectedTransport.handleReceive(i)}onReceive(i){this.receive=i}receive=()=>{};handleReceive(i){this.receive(i)}async close(){}}var wi=I;class k{options;receiveHandler;subIds=[];subjectById=new Map;constructor(i){this.options=i,this.subIds=i.subscriptions.map((n)=>Y.instance.subscribe(n,(R)=>{if(this.receiveHandler)this.receiveHandler(R)}))}async send(i){let n=this.options.publishSubject,R;if(i.t==="end")R=this.subjectById.get(i.c);else R=typeof n==="string"?n:n(i);if(R===void 0)throw Error("Could not get subject");if(i.t==="end")this.subjectById.delete(i.c);else this.subjectById.set(i.c,R);if(Y.instance.publish(R,i)===0)throw Error(`No subscribers for subject: ${R}`);return}onReceive(i){this.receiveHandler=i}async close(){for(let i of this.subIds)Y.instance.unsubscribe(i);this.subIds=[]}}class Y{static instance=new Y;subIdIter=function*(){let n=0;while(!0)n+=1,yield n}();subscriptions={};publish(i,n){let R=this.subscriptions[i]??[];for(let t of R){let w=f(W(n));t.handler(w)}return R.length}subscribe(i,n){let R=this.subscriptions[i]??[],t=this.subIdIter.next().value;return R.push({subId:t,handler:n}),this.subscriptions[i]=R,t}unsubscribe(i){for(let n in this.subscriptions)if(this.subscriptions[n]){if(this.subscriptions[n]=this.subscriptions[n].filter((R)=>R.subId!==i),this.subscriptions[n].length===0)delete this.subscriptions[n]}}}var ri=k;import{createLogger as Ai}from"@phnq/log";import{connect as Oi,JSONCodec as Wi}from"nats";import d from"object-hash";import{v4 as fi}from"uuid";var $=Ai("NATSTransport"),j=process.env.PHNQ_MESSAGE_LOG_NATS==="1",z=new Uint8Array(Buffer.from("@phnq/message/chunk","utf-8")),Z=new Map,y=Wi(),Ci=async(i)=>{let n=i.maxConnectAttempts||1,R=i.connectTimeWait||2000,t=0;while(n===-1||t<n){try{return await Oi(i)}catch(w){if(n===1)throw w;$.error("NATS connection failed: ",w),$("Retrying in %d ms",R),await Di(R)}t+=1}throw Error(`NATS connection failed after ${t} attempts`)},Di=(i)=>new Promise((n)=>setTimeout(n,i));class S{static async create(i,n){let[R,t]=Z.get(d(i))||[await Ci(i),0];Z.set(d(i),[R,t+1]);let w=new S(i,R,n);return w.initialize(),w}config;nc;options;maxPayload;receiveHandler;subjectById=new Map;chunkedMessages=new Map;constructor(i,n,R){if(this.config=i,this.nc=n,this.options=R,this.maxPayload=this.nc.info?.max_payload||0,this.maxPayload===0)throw Error("NATS max_payload not set")}async close(){let i=d(this.config),n=Z.get(i);if(n){let[R,t]=n;if(t>1)Z.set(i,[R,t-1]);else $("Closing NATS connection: ",this.config),await this.nc.close(),Z.delete(i)}}async getConnections(){if(!this.config.monitorUrl)throw Error("monitorUrl not set");return(await(await fetch([this.config.monitorUrl,"connz"].join("/"),{headers:{Connection:"close"}})).json()).connections}async send(i){let n=this.options.publishSubject,R;if(i.t==="end")R=this.subjectById.get(i.c);else R=typeof n==="string"?n:n(i);if(R===void 0)throw Error("Could not get subject");if(i.t==="end")this.subjectById.delete(i.c);else this.subjectById.set(i.c,R);if(j)$("PUBLISH [%s] %O",R,i);let t=this.marshall(i);if(t.length>this.maxPayload)this.sendMessageInChunks(R,t);else this.nc.publish(R,t)}onReceive(i){this.receiveHandler=i}marshall(i){return new Uint8Array(y.encode(W(i)))}unmarshall(i){return f(y.decode(i))}initialize(){this.options.subscriptions.forEach(async(i)=>{let n=typeof i==="string"?i:i.subject,R=typeof i==="string"?void 0:i.options,t=this.nc.subscribe(n,R);for await(let w of t)if(this.receiveHandler){let r=w.data;if(z.some((o,A)=>r[A]!==o)){let o=this.unmarshall(r);if(j)$("RECEIVE [%s] %O",n,o);this.receiveHandler(o)}else this.receiveMessageChunk(r)}})}sendMessageInChunks(i,n){let R=z.length+18,t=this.maxPayload-R,w=Math.ceil(n.length/t),r=[];fi(void 0,r);for(let o=0;o<w;o++){let A=[z,new Uint8Array(Buffer.from(r)),new Uint8Array([o,w])],O=new Uint8Array(Buffer.concat(A,R)),q=n.slice(o*t,Math.min((o+1)*t,n.length));this.nc.publish(i,new Uint8Array(Buffer.concat([O,q])))}}receiveMessageChunk(i){if(!this.receiveHandler)return;let n=z.length,R=d(i.slice(n,n+16)),[t,w]=i.slice(n+16,n+18);if(typeof t!=="number"||typeof w!=="number"){$.error("Invalid chunk index or total in message chunk: ",i);return}let r=this.chunkedMessages.get(R);if(!r)r=Array(w),this.chunkedMessages.set(R,r);if(r[t]=i.slice(n+18),r.length===r.filter(Boolean).length)this.receiveHandler(this.unmarshall(new Uint8Array(Buffer.concat(r)))),this.chunkedMessages.delete(R)}}var $i=S;export{F as WebSocketTransport,oi as WebSocketMessageServer,ni as WebSocketMessageClient,$i as NATSTransport,J as MessageType,V as MessageConnection,ri as LocalPubSubTransport,wi as DirectTransport,s as ConversationPerspective,B as ClientWebSocketTransport,H as Anomaly};