@sanity/comlink
Advanced tools
Comparing version 0.0.1 to 0.0.2-canary.0
@@ -1,1 +0,1 @@ | ||
import{v4 as e}from"uuid";import{fromEventObservable as t,setup as s,sendParent as n,assign as a,fromCallback as r,enqueueActions as o,assertEvent as c,raise as i,createActor as d,emit as u}from"xstate";import{defer as p,fromEvent as l,filter as g,map as m,pipe as h,bufferCount as f,concatMap as y,take as b}from"rxjs";const v=(e=[],t={})=>({context:s})=>{const{count:n,matches:a=!0,responseType:r="message.received"}=t;return{count:n,domain:s.domain,from:s.connectTo,matches:a,responseType:r,target:s.target,to:s.name,type:e}},q=p((()=>l(window,"message"))),x=t((({input:e})=>{return q.pipe(g((e=>t=>{const{data:s}=t,n=Array.isArray(e.type)?e.type:[e.type];return(e.matches?n.includes(s.type):!n.includes(s.type))&&s.domain===e.domain&&s.from===e.from&&s.to===e.to&&(!e.target||t.source===e.target)})(e)),m((t=e.responseType,e=>({type:t,message:e}))),e.count?h(f(e.count),y((e=>e)),b(e.count)):h());var t})),k="sanity/channels",T="channel/response",E="channel/heartbeat",I="channel/disconnect",S="channel/handshake/syn",w="channel/handshake/syn-ack",R="channel/handshake/ack",$=()=>s({types:{},actors:{listen:t((({input:e})=>l(window,"message").pipe(g((t=>t.data.type===T&&t.data.responseTo===e.requestId&&!!t.source&&e.sources.has(t.source))),b(e.sources.size))))},actions:{"send message":({context:e},t)=>{const{sources:s,origin:n}=e,{message:a}=t;s.forEach((e=>{e.postMessage(a,{targetOrigin:n})}))},"on success":n((({context:e,self:t})=>(e.response&&e.resolvable?.resolve(e.response),{type:"request.success",requestId:t.id,response:e.response,responseTo:e.responseTo}))),"on fail":n((({context:e,self:t})=>(console.warn(`Received no response to message '${e.type}' on client '${e.from}' (ID: '${e.id}').`),e.resolvable?.reject(new Error("No response received")),{type:"request.failed",requestId:t.id})))},guards:{expectsResponse:({context:e})=>e.expectResponse},delays:{initialTimeout:0,responseTimeout:1e3}}).createMachine({context:({input:t})=>({connectionId:t.connectionId,data:t.data,domain:t.domain,expectResponse:t.expectResponse??!1,from:t.from,id:`msg-${e()}`,origin:t.origin,resolvable:t.resolvable,response:null,responseTo:t.responseTo,sources:"size"in t.sources?t.sources:new Set([t.sources]),to:t.to,type:t.type}),initial:"idle",states:{idle:{after:{initialTimeout:"sending"}},sending:{entry:{type:"send message",params:({context:e})=>{const{connectionId:t,data:s,domain:n,from:a,id:r,responseTo:o,to:c,type:i}=e;return{message:{connectionId:t,data:s,domain:n,from:a,id:r,to:c,type:i,responseTo:o}}}},always:[{guard:"expectsResponse",target:"awaiting"},"success"]},awaiting:{invoke:{src:"listen",input:({context:e})=>({requestId:e.id,sources:e.sources})},after:{responseTimeout:"failed"},on:{message:{actions:a({response:({event:e})=>e.data.data,responseTo:({event:e})=>e.data.responseTo}),target:"success"}}},failed:{type:"final",entry:"on fail"},success:{type:"final",entry:"on success"}},output:({context:e,self:t})=>({requestId:t.id,response:e.response,responseTo:e.responseTo})}),A=r((({sendBack:e,input:t})=>{const s=()=>{e(t.event)};t.immediate&&s();const n=setInterval(s,t.interval);return()=>{clearInterval(n)}})),M=()=>s({types:{},actors:{requestMachine:$(),listen:x,sendBackAtInterval:A},actions:{"buffer message":o((({enqueue:e})=>{e.assign({buffer:({event:e,context:t})=>(c(e,"post"),[...t.buffer,e.data])}),e.emit((({event:e})=>(c(e,"post"),{type:"_buffer.added",message:e.data})))})),"create request":a({requests:({context:t,event:s,spawn:n})=>{c(s,"request");const a=(Array.isArray(s.data)?s.data:[s.data]).map((s=>{const a=`req-${e()}`;return n("requestMachine",{id:a,input:{connectionId:t.connectionId,data:s.data,domain:t.domain,expectResponse:s.expectResponse,from:t.name,origin:t.origin,responseTo:s.responseTo,sources:t.target,to:t.connectTo,type:s.type}})}));return[...t.requests,...a]}}),"emit received message":o((({enqueue:e})=>{e.emit((({event:e})=>(c(e,"message.received"),{type:"_message",message:e.message.data}))),e.emit((({event:e})=>(c(e,"message.received"),{type:e.message.data.type,message:e.message.data})))})),"flush buffer":o((({enqueue:e})=>{e.raise((({context:e})=>({type:"request",data:e.buffer.map((({data:e,type:t})=>({data:e,type:t})))}))),e.emit((({context:e})=>({type:"_buffer.flushed",messages:e.buffer}))),e.assign({buffer:[]})})),post:i((({event:e})=>(c(e,"post"),{type:"request",data:{data:e.data.data,expectResponse:!0,type:e.data.type}}))),"remove request":a({requests:({context:e,event:t})=>(c(t,["request.success","request.failed"]),e.requests.filter((e=>e.id!==t.requestId)))}),respond:i((({event:e})=>(c(e,"response"),{type:"request",data:{data:e.data,type:T,responseTo:e.respondTo}}))),"send handshake ack":i({type:"request",data:{type:R}}),"send disconnect":i((()=>({type:"request",data:{type:I}}))),"send handshake syn":i({type:"request",data:{type:S}}),"set target":a({target:({event:e})=>(c(e,"target.set"),e.target)})},guards:{"has target":({context:e})=>!!e.target,"should send heartbeats":({context:e})=>e.heartbeat}}).createMachine({id:"channel",context:({input:t})=>({id:t.id||`${t.name}-${e()}`,buffer:[],connectionId:`cnx-${e()}`,connectTo:t.connectTo,domain:t.domain??k,heartbeat:t.heartbeat??!1,name:t.name,origin:t.origin,requests:[],target:t.target}),on:{"target.set":{actions:"set target"},"request.success":{actions:"remove request"},"request.failed":{actions:"remove request"}},initial:"idle",states:{idle:{on:{connect:{target:"handshaking",guard:"has target"},post:{actions:"buffer message"}}},handshaking:{invoke:[{src:"sendBackAtInterval",id:"sendSyn",input:()=>({event:{type:"syn"},interval:500,immediate:!0})},{src:"listen",input:e=>v(w,{count:1})(e)}],on:{syn:{actions:"send handshake syn"},request:{actions:"create request"},post:{actions:"buffer message"},"message.received":{target:"connected"},disconnect:{target:"disconnected"}},exit:"send handshake ack"},connected:{entry:"flush buffer",invoke:{src:"listen",input:v([T,E],{matches:!1})},on:{post:{actions:"post"},request:{actions:"create request"},response:{actions:"respond"},"message.received":{actions:"emit received message"},disconnect:{target:"disconnected"}},initial:"heartbeat",states:{heartbeat:{initial:"checking",states:{checking:{always:{guard:"should send heartbeats",target:"sending"}},sending:{on:{"request.failed":{target:"#disconnected"}},invoke:{src:"sendBackAtInterval",id:"sendHeartbeat",input:()=>({event:{type:"post",data:{type:E,data:void 0}},interval:2e3,immediate:!1})}}}}}},disconnected:{id:"disconnected",entry:"send disconnect",on:{request:{actions:"create request"},post:{actions:"buffer message"},connect:{target:"handshaking",guard:"has target"}}}}}),_=t=>{const s=M(),n=t.id||`${t.name}-${e()}`,a=d(s,{input:{...t,id:n}}),r=()=>{a.stop()};return{actor:a,connect:()=>{a.send({type:"connect"})},disconnect:()=>{a.send({type:"disconnect"})},id:n,name:t.name,machine:s,on:(e,t)=>{const{unsubscribe:s}=a.on(e,(e=>{const s=t(e.message.data);s&&a.send({type:"response",respondTo:e.message.id,data:s})}));return s},onStatus:e=>{const t=a.getSnapshot();let s="string"==typeof t.value?t.value:Object.keys(t.value)[0];const{unsubscribe:n}=a.subscribe((t=>{const n="string"==typeof t.value?t.value:Object.keys(t.value)[0];s!==n&&(s=n,e(n))}));return n},post:e=>{a.send({type:"post",data:e})},setTarget:e=>{a.send({type:"target.set",target:e})},start:()=>(a.start(),r),stop:r,get target(){return a.getSnapshot().context.target}}},j=()=>{},F=()=>{const e=new Set,t=new Set,s=e=>{e.disconnect(),setTimeout((()=>{e.stop()}),0)};return{addTarget:n=>{if(e.has(n))return j;if(!e.size||!t.size)return e.add(n),t.forEach((e=>{e.channels.forEach((e=>{e.setTarget(n),e.connect()}))})),()=>{e.delete(n),t.forEach((e=>{e.channels.forEach((e=>{e.target===n&&e.disconnect()}))}))};e.add(n);const a=new Set;return t.forEach((e=>{const t=_({...e.input,target:n});a.add(t),e.channels.add(t),e.subscribers.forEach((({type:e,handler:s,unsubscribers:n})=>{n.push(t.on(e,s))})),e.internalEventSubscribers.forEach((({type:e,handler:s,unsubscribers:n})=>{n.push(t.actor.on(e,s).unsubscribe)})),e.statusSubscribers.forEach((({handler:e,unsubscribers:s})=>{s.push(t.onStatus((s=>e({channel:t.id,status:s}))))})),t.start(),t.connect()})),()=>{e.delete(n),a.forEach((e=>{s(e),t.forEach((t=>{t.channels.delete(e)}))}))}},createConnection:s=>{const n={input:s,channels:new Set,internalEventSubscribers:new Set,statusSubscribers:new Set,subscribers:new Set};t.add(n);const{channels:a,internalEventSubscribers:r,statusSubscribers:o,subscribers:c}=n;if(e.size)e.forEach((e=>{const t=_({...s,target:e});a.add(t)}));else{const e=_(s);a.add(e)}const i=()=>{a.forEach((e=>{e.stop()}))},d=()=>{a.forEach((e=>{e.disconnect()}))};return{connect:()=>(a.forEach((e=>{e.connect()})),d),disconnect:d,on:(e,t)=>{const s=[];a.forEach((n=>{s.push(n.on(e,t))}));const n={type:e,handler:t,unsubscribers:s};return c.add(n),()=>{s.forEach((e=>e())),c.delete(n)}},onStatus:e=>{const t=[];a.forEach((s=>{t.push(s.onStatus((t=>e({channel:s.id,status:t}))))}));const s={handler:e,unsubscribers:t};return o.add(s),()=>{t.forEach((e=>e())),o.delete(s)}},onInternalEvent:(e,t)=>{const s=[];a.forEach((n=>{s.push(n.actor.on(e,t).unsubscribe)}));const n={type:e,handler:t,unsubscribers:s};return r.add(n),()=>{s.forEach((e=>e())),r.delete(n)}},post:e=>{a.forEach((t=>{t.post(e)}))},start:()=>(a.forEach((e=>{e.start()})),i),stop:i}},destroy:()=>{t.forEach((({channels:e})=>{e.forEach((t=>{s(t),e.delete(t)}))}))}}},z=()=>s({types:{},actors:{requestMachine:$(),listen:x},actions:{"buffer message":o((({enqueue:e})=>{e.assign({buffer:({event:e,context:t})=>(c(e,"post"),[...t.buffer,{data:e.data,resolvable:e.resolvable}])}),e.emit((({event:e})=>(c(e,"post"),{type:"_buffer.added",message:e.data})))})),"create request":a({requests:({context:t,event:s,spawn:n})=>{c(s,"request");const a=(Array.isArray(s.data)?s.data:[s.data]).map((s=>{const a=`req-${e()}`;return n("requestMachine",{id:a,input:{connectionId:t.connectionId,data:s.data,domain:t.domain,expectResponse:s.expectResponse,from:t.name,origin:t.origin,resolvable:s.resolvable,responseTo:s.responseTo,sources:t.target,to:t.connectTo,type:s.type}})}));return[...t.requests,...a]}}),"emit heartbeat":u((()=>({type:"_heartbeat"}))),"emit received message":o((({enqueue:e})=>{e.emit((({event:e})=>(c(e,"message.received"),{type:"_message",message:e.message.data}))),e.emit((({event:e})=>(c(e,"message.received"),{type:e.message.data.type,message:e.message.data})))})),"flush buffer":o((({enqueue:e})=>{e.raise((({context:e})=>({type:"request",data:e.buffer.map((({data:e,resolvable:t})=>({data:e.data,type:e.type,expectResponse:!!t,resolvable:t})))}))),e.emit((({context:e})=>({type:"_buffer.flushed",messages:e.buffer.map((({data:e})=>e))}))),e.assign({buffer:[]})})),post:i((({event:e})=>(c(e,"post"),{type:"request",data:{data:e.data.data,expectResponse:!!e.resolvable,type:e.data.type,resolvable:e.resolvable}}))),"remove request":a({requests:({context:e,event:t})=>(c(t,["request.success","request.failed"]),e.requests.filter((e=>e.id!==t.requestId)))}),"send response":i((({event:e})=>(c(e,["message.received","heartbeat.received"]),{type:"request",data:{type:T,responseTo:e.message.data.id,data:void 0}}))),"send handshake syn ack":i({type:"request",data:{type:w}}),"set connection config":a({connectionId:({event:e})=>(c(e,"message.received"),e.message.data.connectionId),target:({event:e})=>(c(e,"message.received"),e.message.source||void 0),origin:({event:e})=>(c(e,"message.received"),e.message.origin)})},guards:{hasSource:({context:e})=>null!==e.target}}).createMachine({id:"node",context:({input:e})=>({buffer:[],connectionId:null,connectTo:e.connectTo,domain:e.domain??k,name:e.name,origin:null,requests:[],target:void 0}),on:{"request.success":{actions:"remove request"},"request.failed":{actions:"remove request"}},initial:"idle",states:{idle:{invoke:{src:"listen",id:"listenForHandshakeSyn",input:v(S,{count:1}),onDone:{target:"handshaking",guard:"hasSource"}},on:{"message.received":{actions:"set connection config"},post:{actions:"buffer message"}}},handshaking:{entry:"send handshake syn ack",invoke:[{src:"listen",id:"listenForHandshakeAck",input:v(R,{count:1}),onDone:"connected"},{src:"listen",id:"listenForDisconnect",input:v([I],{count:1,responseType:"disconnect"})}],on:{request:{actions:"create request"},post:{actions:"buffer message"},disconnect:{target:"idle"}}},connected:{entry:"flush buffer",invoke:[{src:"listen",id:"listenForMessages",input:v([T,E],{matches:!1})},{src:"listen",id:"listenForHeartbeats",input:v([E],{responseType:"heartbeat.received"})},{src:"listen",id:"listenForDisconnect",input:v([I],{count:1,responseType:"disconnect"})}],on:{request:{actions:"create request"},post:{actions:"post"},disconnect:{target:"idle"},"message.received":{actions:["send response","emit received message"]},"heartbeat.received":{actions:["send response","emit heartbeat"]}}}}}),D=e=>{const t=z(),s=d(t,{input:e}),n=()=>{s.stop()};return{actor:s,fetch:e=>{const t=Promise.withResolvers();return s.send({type:"post",data:e,resolvable:t}),t.promise},machine:t,on:(e,t)=>{const{unsubscribe:n}=s.on(e,(e=>{t(e.message.data)}));return n},onStatus:e=>{const t=s.getSnapshot();let n="string"==typeof t.value?t.value:Object.keys(t.value)[0];const{unsubscribe:a}=s.subscribe((t=>{const s="string"==typeof t.value?t.value:Object.keys(t.value)[0];n!==s&&(n=s,e(s))}));return a},post:e=>{s.send({type:"post",data:e})},start:()=>(s.start(),n),stop:n}};export{E as MSG_HEARTBEAT,T as MSG_RESPONSE,_ as createChannel,M as createChannelMachine,F as createController,D as createNode,z as createNodeMachine,$ as createRequestMachine};//# sourceMappingURL=index.js.map | ||
import{v4 as e}from"uuid";import{fromEventObservable as t,setup as s,sendTo as n,assign as a,fromCallback as r,enqueueActions as o,assertEvent as c,raise as i,stopChild as d,createActor as u,emit as p}from"xstate";import{defer as l,fromEvent as g,map as f,pipe as m,filter as h,bufferCount as b,concatMap as y,take as v,EMPTY as q,takeUntil as k}from"rxjs";const x=e=>({context:t})=>{const{count:s,include:n,exclude:a,responseType:r="message.received"}=e;return{count:s,domain:t.domain,from:t.connectTo,include:n?Array.isArray(n)?n:[n]:[],exclude:a?Array.isArray(a)?a:[a]:[],responseType:r,target:t.target,to:t.name}},T=l((()=>g(window,"message"))),E=e=>t((({input:t})=>{return T.pipe(e?f(e):m(),h((e=>t=>{const{data:s}=t;return(!e.include.length||e.include.includes(s.type))&&(!e.exclude.length||!e.exclude.includes(s.type))&&s.domain===e.domain&&s.from===e.from&&s.to===e.to&&(!e.target||t.source===e.target)})(t)),f((s=t.responseType,e=>({type:s,message:e}))),t.count?m(b(t.count),y((e=>e)),v(t.count)):m());var s})),I="sanity/comlink",w=1e3,S=1e3,R=500,O="comlink/response",$="comlink/heartbeat",A="comlink/disconnect",B="comlink/handshake/syn",M="comlink/handshake/syn-ack",j="comlink/handshake/ack",_=[B,M,j],z=[O,A,$,..._],D=()=>s({types:{},actors:{listen:t((({input:e})=>{const t=e.signal?g(e.signal,"abort").pipe((s=`Request ${e.requestId} aborted`,e=>e.pipe(v(1),f((()=>{throw new Error(s)}))))):q;var s;return g(window,"message").pipe(h((t=>t.data?.type===O&&t.data?.responseTo===e.requestId&&!!t.source&&e.sources.has(t.source))),v(e.sources.size),k(t))}))},actions:{"send message":({context:e},t)=>{const{sources:s,targetOrigin:n}=e,{message:a}=t;s.forEach((e=>{e.postMessage(a,{targetOrigin:n})}))},"on success":n((({context:e})=>e.parentRef),(({context:e,self:t})=>(e.response&&e.resolvable?.resolve(e.response),{type:"request.success",requestId:t.id,response:e.response,responseTo:e.responseTo}))),"on fail":n((({context:e})=>e.parentRef),(({context:e,self:t})=>(console.warn(`Received no response to message '${e.type}' on client '${e.from}' (ID: '${e.id}').`),e.resolvable?.reject(new Error("No response received")),{type:"request.failed",requestId:t.id}))),"on abort":n((({context:e})=>e.parentRef),(({context:e,self:t})=>(e.resolvable?.reject(new Error("Request aborted")),{type:"request.aborted",requestId:t.id})))},guards:{expectsResponse:({context:e})=>e.expectResponse},delays:{initialTimeout:0,responseTimeout:1e3}}).createMachine({context:({input:t})=>({connectionId:t.connectionId,data:t.data,domain:t.domain,expectResponse:t.expectResponse??!1,from:t.from,id:`msg-${e()}`,parentRef:t.parentRef,resolvable:t.resolvable,response:null,responseTo:t.responseTo,signal:t.signal,sources:t.sources instanceof Set?t.sources:new Set([t.sources]),targetOrigin:t.targetOrigin,to:t.to,type:t.type}),initial:"idle",on:{abort:".aborted"},states:{idle:{after:{initialTimeout:[{target:"sending"}]}},sending:{entry:{type:"send message",params:({context:e})=>{const{connectionId:t,data:s,domain:n,from:a,id:r,responseTo:o,to:c,type:i}=e;return{message:{connectionId:t,data:s,domain:n,from:a,id:r,to:c,type:i,responseTo:o}}}},always:[{guard:"expectsResponse",target:"awaiting"},"success"]},awaiting:{invoke:{id:"listen for response",src:"listen",input:({context:e})=>({requestId:e.id,sources:e.sources,signal:e.signal}),onError:"aborted"},after:{responseTimeout:"failed"},on:{message:{actions:a({response:({event:e})=>e.data.data,responseTo:({event:e})=>e.data.responseTo}),target:"success"}}},failed:{type:"final",entry:"on fail"},success:{type:"final",entry:"on success"},aborted:{type:"final",entry:"on abort"}},output:({context:e,self:t})=>({requestId:t.id,response:e.response,responseTo:e.responseTo})}),C=r((({sendBack:e,input:t})=>{const s=()=>{e(t.event)};t.immediate&&s();const n=setInterval(s,t.interval);return()=>{clearInterval(n)}})),N=()=>s({types:{},actors:{requestMachine:D(),listen:E(),sendBackAtInterval:C},actions:{"buffer message":o((({enqueue:e})=>{e.assign({buffer:({event:e,context:t})=>(c(e,"post"),[...t.buffer,e.data])}),e.emit((({event:e})=>(c(e,"post"),{type:"_buffer.added",message:e.data})))})),"create request":a({requests:({context:t,event:s,self:n,spawn:a})=>{c(s,"request");const r=(Array.isArray(s.data)?s.data:[s.data]).map((s=>{const r=`req-${e()}`;return a("requestMachine",{id:r,input:{connectionId:t.connectionId,data:s.data,domain:t.domain,expectResponse:s.expectResponse,from:t.name,parentRef:n,responseTo:s.responseTo,sources:t.target,targetOrigin:t.targetOrigin,to:t.connectTo,type:s.type}})}));return[...t.requests,...r]}}),"emit received message":o((({enqueue:e})=>{e.emit((({event:e})=>(c(e,"message.received"),{type:"_message",message:e.message.data}))),e.emit((({event:e})=>(c(e,"message.received"),{type:e.message.data.type,message:e.message.data})))})),"flush buffer":o((({enqueue:e})=>{e.raise((({context:e})=>({type:"request",data:e.buffer.map((({data:e,type:t})=>({data:e,type:t})))}))),e.emit((({context:e})=>({type:"_buffer.flushed",messages:e.buffer}))),e.assign({buffer:[]})})),post:i((({event:e})=>(c(e,"post"),{type:"request",data:{data:e.data.data,expectResponse:!0,type:e.data.type}}))),"remove request":o((({context:e,enqueue:t,event:s})=>{c(s,["request.success","request.failed","request.aborted"]),d(s.requestId),t.assign({requests:e.requests.filter((({id:e})=>e!==s.requestId))})})),respond:i((({event:e})=>(c(e,"response"),{type:"request",data:{data:e.data,type:O,responseTo:e.respondTo}}))),"send handshake ack":i({type:"request",data:{type:j}}),"send disconnect":i((()=>({type:"request",data:{type:A}}))),"send handshake syn":i({type:"request",data:{type:B}}),"set target":a({target:({event:e})=>(c(e,"target.set"),e.target)})},guards:{"has target":({context:e})=>!!e.target,"should send heartbeats":({context:e})=>e.heartbeat}}).createMachine({id:"channel",context:({input:t})=>({id:t.id||`${t.name}-${e()}`,buffer:[],connectionId:`cnx-${e()}`,connectTo:t.connectTo,domain:t.domain??I,heartbeat:t.heartbeat??!1,name:t.name,requests:[],target:t.target,targetOrigin:t.targetOrigin}),on:{"target.set":{actions:"set target"},"request.success":{actions:"remove request"},"request.failed":{actions:"remove request"}},initial:"idle",states:{idle:{on:{connect:{target:"handshaking",guard:"has target"},post:{actions:"buffer message"}}},handshaking:{invoke:[{id:"send syn",src:"sendBackAtInterval",input:()=>({event:{type:"syn"},interval:500,immediate:!0})},{id:"listen for handshake",src:"listen",input:e=>x({include:M,count:1})(e)}],on:{syn:{actions:"send handshake syn"},request:{actions:"create request"},post:{actions:"buffer message"},"message.received":{target:"connected"},disconnect:{target:"disconnected"}},exit:"send handshake ack"},connected:{entry:"flush buffer",invoke:{id:"listen for messages",src:"listen",input:x({exclude:[O,$]})},on:{post:{actions:"post"},request:{actions:"create request"},response:{actions:"respond"},"message.received":{actions:"emit received message"},disconnect:{target:"disconnected"}},initial:"heartbeat",states:{heartbeat:{initial:"checking",states:{checking:{always:{guard:"should send heartbeats",target:"sending"}},sending:{on:{"request.failed":{target:"#disconnected"}},invoke:{id:"send heartbeat",src:"sendBackAtInterval",input:()=>({event:{type:"post",data:{type:$,data:void 0}},interval:2e3,immediate:!1})}}}}}},disconnected:{id:"disconnected",entry:"send disconnect",on:{request:{actions:"create request"},post:{actions:"buffer message"},connect:{target:"handshaking",guard:"has target"}}}}}),P=(t,s=N())=>{const n=t.id||`${t.name}-${e()}`,a=u(s,{input:{...t,id:n}}),r=()=>{a.stop()};return{actor:a,connect:()=>{a.send({type:"connect"})},disconnect:()=>{a.send({type:"disconnect"})},id:n,name:t.name,machine:s,on:(e,t)=>{const{unsubscribe:s}=a.on(e,(async e=>{const s=await t(e.message.data);s&&a.send({type:"response",respondTo:e.message.id,data:s})}));return s},onStatus:e=>{const t=a.getSnapshot();let s="string"==typeof t.value?t.value:Object.keys(t.value)[0];const{unsubscribe:n}=a.subscribe((t=>{const n="string"==typeof t.value?t.value:Object.keys(t.value)[0];s!==n&&(s=n,e(n))}));return n},post:e=>{a.send({type:"post",data:e})},setTarget:e=>{a.send({type:"target.set",target:e})},start:()=>(a.start(),r),stop:r,get target(){return a.getSnapshot().context.target}}},F=()=>{},G=e=>{const{targetOrigin:t}=e,s=new Set,n=new Set,a=e=>{e.disconnect(),setTimeout((()=>{e.stop()}),0)};return{addTarget:e=>{if(s.has(e))return F;if(!s.size||!n.size)return s.add(e),n.forEach((t=>{t.channels.forEach((t=>{t.setTarget(e),t.connect()}))})),()=>{s.delete(e),n.forEach((t=>{t.channels.forEach((t=>{t.target===e&&t.disconnect()}))}))};s.add(e);const r=new Set;return n.forEach((s=>{const n=P({...s.input,target:e,targetOrigin:t},s.machine);r.add(n),s.channels.add(n),s.subscribers.forEach((({type:e,handler:t,unsubscribers:s})=>{s.push(n.on(e,t))})),s.internalEventSubscribers.forEach((({type:e,handler:t,unsubscribers:s})=>{s.push(n.actor.on(e,t).unsubscribe)})),s.statusSubscribers.forEach((({handler:e,unsubscribers:t})=>{t.push(n.onStatus((t=>e({channel:n.id,status:t}))))})),n.start(),n.connect()})),()=>{s.delete(e),r.forEach((e=>{a(e),n.forEach((t=>{t.channels.delete(e)}))}))}},createConnection:(e,a=N())=>{const r={channels:new Set,input:e,internalEventSubscribers:new Set,machine:a,statusSubscribers:new Set,subscribers:new Set};n.add(r);const{channels:o,internalEventSubscribers:c,statusSubscribers:i,subscribers:d}=r;if(s.size)s.forEach((s=>{const n=P({...e,target:s,targetOrigin:t},a);o.add(n),n.start(),n.connect()}));else{const s=P({...e,targetOrigin:t},a);o.add(s)}const u=()=>{o.forEach((e=>{e.stop()}))},p=()=>{o.forEach((e=>{e.disconnect()}))};return{connect:()=>(o.forEach((e=>{e.connect()})),p),disconnect:p,on:(e,t)=>{const s=[];o.forEach((n=>{s.push(n.on(e,t))}));const n={type:e,handler:t,unsubscribers:s};return d.add(n),()=>{s.forEach((e=>e())),d.delete(n)}},onStatus:e=>{const t=[];o.forEach((s=>{t.push(s.onStatus((t=>e({channel:s.id,status:t}))))}));const s={handler:e,unsubscribers:t};return i.add(s),()=>{t.forEach((e=>e())),i.delete(s)}},onInternalEvent:(e,t)=>{const s=[];o.forEach((n=>{s.push(n.actor.on(e,t).unsubscribe)}));const n={type:e,handler:t,unsubscribers:s};return c.add(n),()=>{s.forEach((e=>e())),c.delete(n)}},post:e=>{o.forEach((t=>{t.post(e)}))},start:()=>(o.forEach((e=>{e.start()})),u),stop:u}},destroy:()=>{n.forEach((({channels:e})=>{e.forEach((t=>{a(t),e.delete(t)}))}))}}},H=()=>s({types:{},actors:{requestMachine:D(),listen:E()},actions:{"buffer incoming message":a({handshakeBuffer:({event:e,context:t})=>(c(e,"message.received"),[...t.handshakeBuffer,e])}),"buffer message":o((({enqueue:e})=>{e.assign({buffer:({event:e,context:t})=>(c(e,"post"),[...t.buffer,{data:e.data,resolvable:e.resolvable,signal:e.signal}])}),e.emit((({event:e})=>(c(e,"post"),{type:"_buffer.added",message:e.data})))})),"create request":a({requests:({context:t,event:s,self:n,spawn:a})=>{c(s,"request");const r=(Array.isArray(s.data)?s.data:[s.data]).map((s=>{const r=`req-${e()}`;return a("requestMachine",{id:r,input:{connectionId:t.connectionId,data:s.data,domain:t.domain,expectResponse:s.expectResponse,from:t.name,parentRef:n,resolvable:s.resolvable,responseTo:s.responseTo,sources:t.target,targetOrigin:t.targetOrigin,to:t.connectTo,type:s.type,signal:s.signal}})}));return[...t.requests,...r]}}),"emit heartbeat":p((()=>({type:"_heartbeat"}))),"emit received message":o((({enqueue:e})=>{e.emit((({event:e})=>(c(e,"message.received"),{type:"_message",message:e.message.data}))),e.emit((({event:e})=>(c(e,"message.received"),{type:e.message.data.type,message:e.message.data})))})),"flush buffer":o((({enqueue:e})=>{e.raise((({context:e})=>({type:"request",data:e.buffer.map((({data:e,resolvable:t,signal:s})=>({data:e.data,type:e.type,expectResponse:!!t,resolvable:t,signal:s})))}))),e.emit((({context:e})=>({type:"_buffer.flushed",messages:e.buffer.map((({data:e})=>e))}))),e.assign({buffer:[]})})),"flush handshake buffer":o((({context:e,enqueue:t})=>{e.handshakeBuffer.forEach((e=>t.raise(e))),t.assign({handshakeBuffer:[]})})),post:i((({event:e})=>(c(e,"post"),{type:"request",data:{data:e.data.data,expectResponse:!!e.resolvable,type:e.data.type,resolvable:e.resolvable,signal:e.signal}}))),"remove request":o((({context:e,enqueue:t,event:s})=>{c(s,["request.success","request.failed","request.aborted"]),d(s.requestId),t.assign({requests:e.requests.filter((({id:e})=>e!==s.requestId))})})),"send response":i((({event:e})=>(c(e,["message.received","heartbeat.received"]),{type:"request",data:{type:O,responseTo:e.message.data.id,data:void 0}}))),"send handshake syn ack":i({type:"request",data:{type:M}}),"set connection config":a({connectionId:({event:e})=>(c(e,"message.received"),e.message.data.connectionId),target:({event:e})=>(c(e,"message.received"),e.message.source||void 0),targetOrigin:({event:e})=>(c(e,"message.received"),e.message.origin)})},guards:{hasSource:({context:e})=>null!==e.target}}).createMachine({id:"node",context:({input:e})=>({buffer:[],connectionId:null,connectTo:e.connectTo,domain:e.domain??I,handshakeBuffer:[],name:e.name,requests:[],target:void 0,targetOrigin:null}),on:{"request.success":{actions:"remove request"},"request.failed":{actions:"remove request"},"request.aborted":{actions:"remove request"}},initial:"idle",states:{idle:{invoke:{id:"listen for handshake syn",src:"listen",input:x({include:B,count:1}),onDone:{target:"handshaking",guard:"hasSource"}},on:{"message.received":{actions:"set connection config"},post:{actions:"buffer message"}}},handshaking:{entry:"send handshake syn ack",invoke:[{id:"listen for handshake ack",src:"listen",input:x({include:j,count:1}),onDone:"connected"},{id:"listen for disconnect",src:"listen",input:x({include:A,count:1,responseType:"disconnect"})},{id:"listen for messages",src:"listen",input:x({exclude:[A,j,$,O]})}],on:{request:{actions:"create request"},post:{actions:"buffer message"},"message.received":{actions:"buffer incoming message"},disconnect:{target:"idle"}}},connected:{entry:["flush handshake buffer","flush buffer"],invoke:[{id:"listen for messages",src:"listen",input:x({exclude:[O,$]})},{id:"listen for heartbeat",src:"listen",input:x({include:$,responseType:"heartbeat.received"})},{id:"listen for disconnect",src:"listen",input:x({include:A,count:1,responseType:"disconnect"})}],on:{request:{actions:"create request"},post:{actions:"post"},disconnect:{target:"idle"},"message.received":{actions:["send response","emit received message"]},"heartbeat.received":{actions:["send response","emit heartbeat"]}}}}}),J=(e,t=H())=>{const s=u(t,{input:e}),n=()=>{s.stop()};return{actor:s,fetch:(e,t)=>{const n=Promise.withResolvers();return s.send({type:"post",data:e,resolvable:n,signal:t?.signal}),n.promise},machine:t,on:(e,t)=>{const{unsubscribe:n}=s.on(e,(e=>{t(e.message.data)}));return n},onStatus:e=>{const t=s.getSnapshot();let n="string"==typeof t.value?t.value:Object.keys(t.value)[0];const{unsubscribe:a}=s.subscribe((t=>{const s="string"==typeof t.value?t.value:Object.keys(t.value)[0];n!==s&&(n=s,e(s))}));return a},post:e=>{s.send({type:"post",data:e})},start:()=>(s.start(),n),stop:n}};export{I as DOMAIN,R as HANDSHAKE_INTERVAL,_ as HANDSHAKE_MSG_TYPES,S as HEARTBEAT_INTERVAL,z as INTERNAL_MSG_TYPES,A as MSG_DISCONNECT,j as MSG_HANDSHAKE_ACK,B as MSG_HANDSHAKE_SYN,M as MSG_HANDSHAKE_SYN_ACK,$ as MSG_HEARTBEAT,O as MSG_RESPONSE,w as RESPONSE_TIMEOUT,P as createChannel,N as createChannelMachine,G as createController,E as createListenLogic,J as createNode,H as createNodeMachine,D as createRequestMachine};//# sourceMappingURL=index.js.map |
{ | ||
"name": "@sanity/comlink", | ||
"version": "0.0.1", | ||
"version": "0.0.2-canary.0", | ||
"keywords": [ | ||
@@ -40,41 +40,5 @@ "sanity.io", | ||
"eslintConfig": { | ||
"parser": "@typescript-eslint/parser", | ||
"parserOptions": { | ||
"ecmaFeatures": { | ||
"jsx": true | ||
}, | ||
"ecmaVersion": 2018, | ||
"sourceType": "module" | ||
}, | ||
"plugins": [ | ||
"@typescript-eslint", | ||
"simple-import-sort", | ||
"prettier" | ||
], | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:prettier/recommended", | ||
"plugin:@typescript-eslint/eslint-recommended", | ||
"plugin:@typescript-eslint/recommended" | ||
"@repo/eslint-config" | ||
], | ||
"rules": { | ||
"no-console": "error", | ||
"no-warning-comments": [ | ||
"warn", | ||
{ | ||
"location": "start", | ||
"terms": [ | ||
"todo", | ||
"@todo", | ||
"fixme" | ||
] | ||
} | ||
], | ||
"@typescript-eslint/explicit-module-boundary-types": "error", | ||
"@typescript-eslint/member-delimiter-style": "off", | ||
"@typescript-eslint/no-empty-interface": "off", | ||
"prettier/prettier": "warn", | ||
"simple-import-sort/exports": "warn", | ||
"simple-import-sort/imports": "warn" | ||
}, | ||
"root": true | ||
@@ -88,10 +52,6 @@ }, | ||
"devDependencies": { | ||
"@typescript-eslint/eslint-plugin": "^7.18.0", | ||
"@typescript-eslint/parser": "^7.18.0", | ||
"eslint": "^8.57.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-plugin-prettier": "^5.2.1", | ||
"eslint-plugin-simple-import-sort": "^12.1.1", | ||
"eslint": "^8.57.1", | ||
"typescript": "5.6.2", | ||
"vitest": "^2.0.5", | ||
"vitest": "^2.1.1", | ||
"@repo/eslint-config": "0.0.0", | ||
"@repo/package.config": "0.0.0" | ||
@@ -108,5 +68,5 @@ }, | ||
"dev": "pkg build --strict", | ||
"lint": "eslint .", | ||
"lint": "eslint --cache .", | ||
"test": "vitest --pass-with-no-tests --typecheck" | ||
} | ||
} |
import {v4 as uuid} from 'uuid' | ||
import { | ||
type ActorRefFrom, | ||
assertEvent, | ||
@@ -8,9 +7,10 @@ assign, | ||
enqueueActions, | ||
type EventObject, | ||
fromCallback, | ||
raise, | ||
setup, | ||
stopChild, | ||
type ActorRefFrom, | ||
type EventObject, | ||
} from 'xstate' | ||
import {listenActor, listenInputFromContext} from './common' | ||
import {createListenLogic, listenInputFromContext} from './common' | ||
import { | ||
@@ -30,7 +30,9 @@ DOMAIN, | ||
BufferFlushedEmitEvent, | ||
Message, | ||
MessageEmitEvent, | ||
ProtocolMessage, | ||
RequestData, | ||
Status, | ||
WithoutResponse, | ||
} from './types' | ||
import type {Message, MessageData, ProtocolMessage} from './types' | ||
@@ -40,2 +42,8 @@ /** | ||
*/ | ||
export type ChannelActorLogic<R extends Message, S extends Message> = ReturnType< | ||
typeof createChannelMachine<R, S> | ||
> | ||
/** | ||
* @public | ||
*/ | ||
export type ChannelActor<R extends Message, S extends Message> = ActorRefFrom< | ||
@@ -57,5 +65,5 @@ ReturnType<typeof createChannelMachine<R, S>> | ||
type: T, | ||
handler: (event: U['data']) => U['response'], | ||
handler: (event: U['data']) => Promise<U['response']> | U['response'], | ||
) => () => void | ||
onStatus: (handler: (status: string) => void) => () => void | ||
onStatus: (handler: (status: Status) => void) => () => void | ||
post: (data: WithoutResponse<S>) => void | ||
@@ -77,4 +85,4 @@ setTarget: (target: MessageEventSource) => void | ||
id?: string | ||
origin: string | ||
target?: MessageEventSource | ||
targetOrigin: string | ||
} | ||
@@ -112,2 +120,8 @@ | ||
types: {} as { | ||
children: { | ||
'listen for handshake': 'listen' | ||
'listen for messages': 'listen' | ||
'send heartbeat': 'sendBackAtInterval' | ||
'send syn': 'sendBackAtInterval' | ||
} | ||
context: { | ||
@@ -121,5 +135,5 @@ buffer: Array<V> | ||
name: string | ||
origin: string | ||
requests: Array<RequestActorRef<S>> | ||
target: MessageEventSource | undefined | ||
targetOrigin: string | ||
} | ||
@@ -137,2 +151,3 @@ emitted: | ||
| {type: 'response'; respondTo: string; data: Pick<S, 'response'>} | ||
| {type: 'request.aborted'; requestId: string} | ||
| {type: 'request.failed'; requestId: string} | ||
@@ -142,3 +157,3 @@ | { | ||
requestId: string | ||
response: MessageData | null | ||
response: S['response'] | null | ||
responseTo: string | undefined | ||
@@ -153,3 +168,3 @@ } | ||
requestMachine: createRequestMachine<S>(), | ||
listen: listenActor, | ||
listen: createListenLogic(), | ||
sendBackAtInterval, | ||
@@ -174,3 +189,3 @@ }, | ||
'create request': assign({ | ||
requests: ({context, event, spawn}) => { | ||
requests: ({context, event, self, spawn}) => { | ||
assertEvent(event, 'request') | ||
@@ -188,5 +203,6 @@ const arr = Array.isArray(event.data) ? event.data : [event.data] | ||
from: context.name, | ||
origin: context.origin, | ||
parentRef: self, | ||
responseTo: request.responseTo, | ||
sources: context.target!, | ||
targetOrigin: context.targetOrigin, | ||
to: context.connectTo, | ||
@@ -243,7 +259,6 @@ type: request.type, | ||
}), | ||
'remove request': assign({ | ||
requests: ({context, event}) => { | ||
assertEvent(event, ['request.success', 'request.failed']) | ||
return context.requests.filter((request) => request.id !== event.requestId) | ||
}, | ||
'remove request': enqueueActions(({context, enqueue, event}) => { | ||
assertEvent(event, ['request.success', 'request.failed', 'request.aborted']) | ||
stopChild(event.requestId) | ||
enqueue.assign({requests: context.requests.filter(({id}) => id !== event.requestId)}) | ||
}), | ||
@@ -287,3 +302,3 @@ 'respond': raise(({event}) => { | ||
}).createMachine({ | ||
/** @xstate-layout N4IgpgJg5mDOIC5QGMAWBDAdpsAbAxLAPYCuATsmAHSxgAuA2gAwC6ioADkbAJZ09FM7EAA9EAdnFMqAJgCs4gBwA2RTMVqAzNoA0IAJ6IZTaQE4mymQBYrc7TYCM4qwF8XetFhwEyYAI4kcHQ0JMiUsLDMbEggXLz8gsJiCM6msppMVuYyznKKTnqGCA4WVLaaDhmmTpma+W4eGNh4+L4BQVQAZug8uJBRwnF8AkIxyeJycmXiOUxODlbKGVaFiA4yDSCezbhUPBB9+MiCOMiMrIPcw4lja3MyVMpWFnLWS6Z1DqspJVTiDio6qZ5DIKqZNttvHsDmB8HFztFOFcEqNQMlFBU-gpFCYZDJTFZFM5vsomGlxMpJnMbMZ6u4tk0oU0ILAMABrHiYKCEfSYAYxIYopJrDLiKiaOQOZTSuTS0lPb6yh4UyaKOSLSUS1z0yF4KjM1noDlc1r+QKwBGXeIjYXFTTWcVWOpWBwfJ1S76LZTi9Yu7Q45TAykQxl6g3sznc+H8pHWm5okUyByyJgVFSEmxO76aQNUNUfcSmKQLJwbHWh3bho2R-AAWzgsHQMCovkoPAAbv0LgLkTbbsVMmkNOo7HjM4pPeYqHN1UTTGpKSqQ14w1gWRGTRAeLBjs0zjHYr346I1oSqK6S9VTE8mIpr98AbLHkTCcoHA51XjweWV7td6c6EgOFuEtHs41RE8EDkJgHkmDV7UsRRbGUb5TDyR5rDVJCJjyTRlx2Kh-zAM4gLac1QNja4IOSKxxE0KhJTQ+072UYtpW+dNz3+f4YOxD4HHwqEiJIiBTVgLhMFoA9BT7BMECyaQFCsUEcnnJZJk0b4KXJAEll9HIiUEvVhMA0T6wiJtqFbMAOy7RFD3A20bEUKhTGBV1r1dRYnBQgxTzxBilA-YEpCYf5tUaX9CJOYjTPwLcdxi-du0ooV+xKdZHmeSk3k0D58gfJNvVJKUqlsPFpSMv8ktM-UwHQMg6AAI3q4I0GI41uWko9qMQe0HjqHIKTqBCYMK6pxXUEtJiLKVlCq6K91q1B6salr0GCWhMC3E0yI6bpejsq0qNtDKHhvHLFjyz5Cspc8ZyeQtaJzOQFoSkzSLNIJusc9LMm9NR8nyedvImB81SmDNlIpKQZEqn8CPemqgOjFKHJOv6nWfCY30qUkcwfCY0lTSowsJSQ3zwhGoSRpbPpMn6MbkmZirc6xnDVQsFnB0oAWcCkshmPEy3pTAiAgOBhF1XBjrSuS8m+dQBrkAt1WgwlZSpyKCP2PpZdkyCiQeAl3IJeQWJWPyfmTJMVFqUECRghaq06-Xj2SX0pjfGZXiQkw5hJJ5ZF0i9M3fBaPogN3eoHXyigysUwpxCY7zyTWI+RiA6oa5rWuj071QfJwxWxW9LGgosFAzums5WnP1ralbkFdsCmcg4wZEKjRAvyd9lMpeHtaEzPs7W1qaDAbbI3z-t1WTTQnXed8woUL4rfWO6k7JJhoNdMm3u3SOZ7ks6speXL8rX+O0O9VXaLVIbKjcNwgA */ | ||
/** @xstate-layout N4IgpgJg5mDOIC5QGMAWBDAdpsAbAxAC7oBOMhAdLGIQNoAMAuoqAA4D2sAloV+5ixAAPRAHZRAJgoAWABz0ArHICMy2QGZZCgJwAaEAE9EE+tIrb6ANgkLl46fTuj1AXxf60WHARJgAjgCucJSwAcjIcLAMzEggHNy8-IIiCKLS2hQS6qb2yurisrL6RgjK9LIyCuqq0g7WstZuHhjYePi+gcEUAGboXLiQ0YLxPHwCsSmiCgoykpayDtqS6trqxYjKEk0gnq24FFwQA-jI-DjIdEzDnKNJExuOZpZ12eq29OrSCuupypYUojUaTKCnm5Wk2123gORzA+HilxibBuiXGoBSGnUAIU4gU9FWamUtR+lmUM1EllBEkslMUEnpkJa0JaEFgGAA1lxMFB8LADJghrERqjkhtshk3mTtNo5OpqpYfqCKhTptoqpY1WUtu4dky8BQWWz0Jzue1-EFYIjrgkxqLSupqRRPpoPqJtLI0hIioZENJJE7NnJ8ZYHVk1YyvPrDRyuTyEYLkTa7uixVlMh81KGFhS1j6EPkZlpVjTphr8mkI3sDVhWTHTQBbSLoGAUXwRLgAN0GVyFKNt91KimUFEKXvKC2s9R+6X+jipnzJeSqEJ1UKjNaNJp5EC4sFOrQuCbifeTwg2cgoym0RPxDtqkj0eaB9Ao8zSolMEivZVcq71+33c5CEgeFOCtXskzRM8EDxKRpmkSw3QJbQsmpH5tHmV8JHSbJpDsakV2aSMALOMALhAjoLXAxNbiglI-SxWw1Vw0QNDw0Qfg9KQ7EJSxHHxApK2hQCyOAiAzVgDhMGoI9hX7FMEHSF8cWkelpHURCbBsb481xAEgT9BQJCmWQsiE-URPI8TG1gWBmzAVsyLATtuyRY9ILtWoKmlL82Kqd0tAVJ91LMHFZDKIkVlkNVZHMkiDzE-Adz3UjDx7GiRQHCKnheD53k+HSSkDDIwpBVTqQwuKKEssSDTAUhCAAI3qyg0DIrd8Fkk86MQUMnVM+RynoegTDJH48hGp0vR-FDRqqKqasgOqGua9AQjATAd1NSiul6fpXOtWi7Wy19cslD4vnG7IX3oVjVDUVYEJQqrksW8SdstLqPKy0wKgG1RhtMWogqKhoMjkWp6XxUyFBe3c3tAz70vco6fq+V8PTkGUFzdQqNnELEM2yClrwwzQ4ZShKQJqr7UYU98AS0W9pT4z5pHG0yXwMkNNTyGk3B1TB2AgOBBDXXBDsyhSFG9EovQqN5i1JeRcKqw4Bkl+ToMx8x0j+EaqQ9XMSkBURMgMkEwQWKro2NWNNdPFJAzN0lJGM4slDxhBEJfXyplBd03wW1KxIdnrBxBh4JAyW75C8rJpmDqmIGWkgmpasPjqUcaHooMLHA0uU1UkJOgKW1B6rT1bWor5At0zgcTAkK7hrz1irB0D8cW0UvRPLyv07WqgNq2qAG+l9SnXUz0UOXD5xuMs3Y4+DVJBX7UiKrV6Q8gcfoJO54rFefLLqfJYX1WKYNLxL4NO1NwgA */ | ||
id: 'channel', | ||
@@ -298,5 +313,5 @@ context: ({input}) => ({ | ||
name: input.name, | ||
origin: input.origin, | ||
requests: [], | ||
target: input.target, | ||
targetOrigin: input.targetOrigin, | ||
}), | ||
@@ -330,4 +345,4 @@ on: { | ||
{ | ||
id: 'send syn', | ||
src: 'sendBackAtInterval', | ||
id: 'sendSyn', | ||
input: () => ({ | ||
@@ -340,5 +355,7 @@ event: {type: 'syn'}, | ||
{ | ||
id: 'listen for handshake', | ||
src: 'listen', | ||
input: (input) => | ||
listenInputFromContext(MSG_HANDSHAKE_SYN_ACK, { | ||
listenInputFromContext({ | ||
include: MSG_HANDSHAKE_SYN_ACK, | ||
count: 1, | ||
@@ -377,4 +394,7 @@ })(input), | ||
invoke: { | ||
id: 'listen for messages', | ||
src: 'listen', | ||
input: listenInputFromContext([MSG_RESPONSE, MSG_HEARTBEAT], {matches: false}), | ||
input: listenInputFromContext({ | ||
exclude: [MSG_RESPONSE, MSG_HEARTBEAT], | ||
}), | ||
}, | ||
@@ -416,4 +436,4 @@ on: { | ||
invoke: { | ||
id: 'send heartbeat', | ||
src: 'sendBackAtInterval', | ||
id: 'sendHeartbeat', | ||
input: () => ({ | ||
@@ -457,5 +477,4 @@ event: {type: 'post', data: {type: MSG_HEARTBEAT, data: undefined}}, | ||
input: ChannelInput, | ||
machine: ChannelActorLogic<R, S> = createChannelMachine<R, S>(), | ||
): Channel<R, S> => { | ||
const machine = createChannelMachine<R, S>() | ||
const id = input.id || `${input.name}-${uuid()}` | ||
@@ -468,3 +487,3 @@ const actor = createActor(machine, { | ||
type: T, | ||
handler: (event: U['data']) => U['response'], | ||
handler: (event: U['data']) => Promise<U['response']> | U['response'], | ||
) => { | ||
@@ -474,4 +493,4 @@ const {unsubscribe} = actor.on( | ||
type, | ||
(event: {type: T; message: ProtocolMessage<U>}) => { | ||
const response = handler(event.message.data) | ||
async (event: {type: T; message: ProtocolMessage<U>}) => { | ||
const response = await handler(event.message.data) | ||
if (response) { | ||
@@ -493,5 +512,5 @@ actor.send({type: 'response', respondTo: event.message.id, data: response}) | ||
const onStatus = (handler: (status: string) => void) => { | ||
const onStatus = (handler: (status: Status) => void) => { | ||
const currentSnapshot = actor.getSnapshot() | ||
let currentStatus: string | undefined = | ||
let currentStatus: Status = | ||
typeof currentSnapshot.value === 'string' | ||
@@ -502,3 +521,4 @@ ? currentSnapshot.value | ||
const {unsubscribe} = actor.subscribe((state) => { | ||
const status = typeof state.value === 'string' ? state.value : Object.keys(state.value)[0] | ||
const status: Status = | ||
typeof state.value === 'string' ? state.value : Object.keys(state.value)[0] | ||
if (currentStatus !== status) { | ||
@@ -505,0 +525,0 @@ currentStatus = status |
import {bufferCount, concatMap, defer, filter, fromEvent, map, pipe, take} from 'rxjs' | ||
import {fromEventObservable} from 'xstate' | ||
import type {ListenInput, ProtocolMessage} from './types' | ||
@@ -8,4 +7,16 @@ | ||
( | ||
type: string | string[] = [], | ||
options: {matches?: boolean; count?: number; responseType?: string} = {}, | ||
config: ( | ||
| { | ||
include: string | string[] | ||
exclude?: string | string[] | ||
} | ||
| { | ||
include?: string | string[] | ||
exclude: string | string[] | ||
} | ||
) & { | ||
matches?: boolean | ||
count?: number | ||
responseType?: string | ||
}, | ||
) => | ||
@@ -24,3 +35,3 @@ < | ||
}): ListenInput => { | ||
const {count, matches = true, responseType = 'message.received'} = options | ||
const {count, include, exclude, responseType = 'message.received'} = config | ||
return { | ||
@@ -30,7 +41,7 @@ count, | ||
from: context.connectTo, | ||
matches, | ||
include: include ? (Array.isArray(include) ? include : [include]) : [], | ||
exclude: exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : [], | ||
responseType, | ||
target: context.target, | ||
to: context.name, | ||
type, | ||
} | ||
@@ -43,5 +54,5 @@ } | ||
const {data} = event | ||
const types = Array.isArray(input.type) ? input.type : [input.type] | ||
return ( | ||
(input.matches ? types.includes(data.type) : !types.includes(data.type)) && | ||
(input.include.length ? input.include.includes(data.type) : true) && | ||
(input.exclude.length ? !input.exclude.includes(data.type) : true) && | ||
data.domain === input.domain && | ||
@@ -65,14 +76,22 @@ data.from === input.from && | ||
export const listenActor = fromEventObservable(({input}: {input: ListenInput}) => { | ||
return messageEvents$.pipe( | ||
filter(listenFilter(input)), | ||
map(eventToMessage(input.responseType)), | ||
input.count | ||
? pipe( | ||
bufferCount(input.count), | ||
concatMap((arr) => arr), | ||
take(input.count), | ||
) | ||
: pipe(), | ||
) | ||
}) | ||
/** | ||
* @public | ||
*/ | ||
export const createListenLogic = ( | ||
compatMap?: (event: MessageEvent<ProtocolMessage>) => MessageEvent<ProtocolMessage>, | ||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types | ||
) => | ||
fromEventObservable(({input}: {input: ListenInput}) => { | ||
return messageEvents$.pipe( | ||
compatMap ? map(compatMap) : pipe(), | ||
filter(listenFilter(input)), | ||
map(eventToMessage(input.responseType)), | ||
input.count | ||
? pipe( | ||
bufferCount(input.count), | ||
concatMap((arr) => arr), | ||
take(input.count), | ||
) | ||
: pipe(), | ||
) | ||
}) |
import type {MessageType} from './types' | ||
/** @internal */ | ||
export const DOMAIN = 'sanity/channels' | ||
export const DOMAIN = 'sanity/comlink' | ||
@@ -18,3 +18,3 @@ /** @internal */ | ||
*/ | ||
export const MSG_RESPONSE = 'channel/response' | ||
export const MSG_RESPONSE = 'comlink/response' | ||
@@ -24,15 +24,15 @@ /** | ||
*/ | ||
export const MSG_HEARTBEAT = 'channel/heartbeat' | ||
export const MSG_HEARTBEAT = 'comlink/heartbeat' | ||
/** @internal */ | ||
export const MSG_DISCONNECT = 'channel/disconnect' | ||
export const MSG_DISCONNECT = 'comlink/disconnect' | ||
/** @internal */ | ||
export const MSG_HANDSHAKE_SYN = 'channel/handshake/syn' | ||
export const MSG_HANDSHAKE_SYN = 'comlink/handshake/syn' | ||
/** @internal */ | ||
export const MSG_HANDSHAKE_SYN_ACK = 'channel/handshake/syn-ack' | ||
export const MSG_HANDSHAKE_SYN_ACK = 'comlink/handshake/syn-ack' | ||
/** @internal */ | ||
export const MSG_HANDSHAKE_ACK = 'channel/handshake/ack' | ||
export const MSG_HANDSHAKE_ACK = 'comlink/handshake/ack' | ||
@@ -39,0 +39,0 @@ /** @internal */ |
@@ -1,6 +0,18 @@ | ||
import {type Channel, type ChannelInput, createChannel} from './channel' | ||
import {type InternalEmitEvent, type Message, type WithoutResponse} from './types' | ||
import { | ||
createChannel, | ||
createChannelMachine, | ||
type Channel, | ||
type ChannelActorLogic, | ||
type ChannelInput, | ||
} from './channel' | ||
import {type InternalEmitEvent, type Message, type StatusEvent, type WithoutResponse} from './types' | ||
export type ConnectionInput = Omit<ChannelInput, 'target'> | ||
/** | ||
* @public | ||
*/ | ||
export type ConnectionInput = Omit<ChannelInput, 'target' | 'targetOrigin'> | ||
/** | ||
* @public | ||
*/ | ||
export interface ConnectionInstance<R extends Message, S extends Message> { | ||
@@ -11,4 +23,4 @@ connect: () => () => void | ||
type: T, | ||
handler: (event: U['data']) => U['response'], | ||
) => void | ||
handler: (event: U['data']) => Promise<U['response']> | U['response'], | ||
) => () => void | ||
onInternalEvent: < | ||
@@ -20,4 +32,4 @@ T extends InternalEmitEvent<R, S>['type'], | ||
handler: (event: U) => void, | ||
) => void | ||
onStatus: (handler: (event: {channel: string; status: string}) => void) => void | ||
) => () => void | ||
onStatus: (handler: (event: StatusEvent) => void) => void | ||
post: (data: WithoutResponse<S>) => void | ||
@@ -35,2 +47,3 @@ start: () => () => void | ||
input: ConnectionInput, | ||
machine?: ChannelActorLogic<R, S>, | ||
) => ConnectionInstance<R, S> | ||
@@ -52,4 +65,5 @@ destroy: () => void | ||
}> | ||
machine: ChannelActorLogic<R, S> | ||
statusSubscribers: Set<{ | ||
handler: (event: {channel: string; status: string}) => void | ||
handler: (event: StatusEvent) => void | ||
unsubscribers: Array<() => void> | ||
@@ -59,3 +73,3 @@ }> | ||
type: R['type'] | ||
handler: (event: R['data']) => R['response'] | ||
handler: (event: R['data']) => Promise<R['response']> | R['response'] | ||
unsubscribers: Array<() => void> | ||
@@ -70,3 +84,4 @@ }> | ||
*/ | ||
export const createController = (): Controller => { | ||
export const createController = (input: {targetOrigin: string}): Controller => { | ||
const {targetOrigin} = input | ||
const targets = new Set<MessageEventSource>() | ||
@@ -76,3 +91,3 @@ const connections = new Set<Connection>() | ||
const addTarget = (target: MessageEventSource) => { | ||
// If the target has already been added, return just return a noop cleanup | ||
// If the target has already been added, return just a noop cleanup | ||
if (targets.has(target)) { | ||
@@ -115,6 +130,10 @@ return noop | ||
connections.forEach((connection) => { | ||
const channel = createChannel({ | ||
...connection.input, | ||
target, | ||
}) | ||
const channel = createChannel( | ||
{ | ||
...connection.input, | ||
target, | ||
targetOrigin, | ||
}, | ||
connection.machine, | ||
) | ||
@@ -162,8 +181,10 @@ targetChannels.add(channel) | ||
const createConnection = <R extends Message, S extends Message>( | ||
input: ChannelInput, | ||
input: ConnectionInput, | ||
machine: ChannelActorLogic<R, S> = createChannelMachine<R, S>(), | ||
): ConnectionInstance<R, S> => { | ||
const connection: Connection<R, S> = { | ||
channels: new Set(), | ||
input, | ||
channels: new Set(), | ||
internalEventSubscribers: new Set(), | ||
machine, | ||
statusSubscribers: new Set(), | ||
@@ -181,15 +202,21 @@ subscribers: new Set(), | ||
targets.forEach((target) => { | ||
const channel = createChannel<R, S>({ | ||
...input, | ||
target, | ||
}) | ||
const channel = createChannel<R, S>( | ||
{ | ||
...input, | ||
target, | ||
targetOrigin, | ||
}, | ||
machine, | ||
) | ||
channels.add(channel) | ||
channel.start() | ||
channel.connect() | ||
}) | ||
} else { | ||
// If targets have not been added yet, create a channel without a target | ||
const channel = createChannel<R, S>(input) | ||
const channel = createChannel<R, S>({...input, targetOrigin}, machine) | ||
channels.add(channel) | ||
} | ||
const post = (data: WithoutResponse<S>) => { | ||
const post: ConnectionInstance<R, S>['post'] = (data) => { | ||
channels.forEach((channel) => { | ||
@@ -200,6 +227,3 @@ channel.post(data) | ||
const on = <T extends R['type'], U extends Extract<R, {type: T}>>( | ||
type: T, | ||
handler: (event: U['data']) => U['response'], | ||
) => { | ||
const on: ConnectionInstance<R, S>['on'] = (type, handler) => { | ||
const unsubscribers: Array<() => void> = [] | ||
@@ -239,3 +263,3 @@ channels.forEach((channel) => { | ||
const onStatus = (handler: (event: {channel: string; status: string}) => void) => { | ||
const onStatus = (handler: (event: StatusEvent) => void) => { | ||
const unsubscribers: Array<() => void> = [] | ||
@@ -242,0 +266,0 @@ channels.forEach((channel) => { |
export { | ||
type Channel, | ||
type ChannelActor, | ||
type ChannelActorLogic, | ||
type ChannelInput, | ||
@@ -8,3 +9,4 @@ createChannel, | ||
} from './channel' | ||
export {MSG_HEARTBEAT, MSG_RESPONSE} from './constants' | ||
export {createListenLogic} from './common' | ||
export * from './constants' | ||
export { | ||
@@ -16,3 +18,10 @@ type ConnectionInput, | ||
} from './controller' | ||
export {createNode, createNodeMachine, type Node, type NodeActor, type NodeInput} from './node' | ||
export { | ||
createNode, | ||
createNodeMachine, | ||
type Node, | ||
type NodeActor, | ||
type NodeActorLogic, | ||
type NodeInput, | ||
} from './node' | ||
export {createRequestMachine, type RequestActorRef, type RequestMachineContext} from './request' | ||
@@ -22,5 +31,8 @@ export type { | ||
BufferFlushedEmitEvent, | ||
DisconnectMessage, | ||
HandshakeMessageType, | ||
HeartbeatEmitEvent, | ||
HeartbeatMessage, | ||
InternalEmitEvent, | ||
InternalMessageType, | ||
ListenInput, | ||
@@ -34,3 +46,5 @@ Message, | ||
ResponseMessage, | ||
Status, | ||
StatusEvent, | ||
WithoutResponse, | ||
} from './types' |
196
src/node.ts
import {v4 as uuid} from 'uuid' | ||
import { | ||
type ActorRefFrom, | ||
assertEvent, | ||
@@ -11,5 +10,6 @@ assign, | ||
setup, | ||
stopChild, | ||
type ActorRefFrom, | ||
} from 'xstate' | ||
import {listenActor, listenInputFromContext} from './common' | ||
import {createListenLogic, listenInputFromContext} from './common' | ||
import { | ||
@@ -29,7 +29,10 @@ DOMAIN, | ||
HeartbeatEmitEvent, | ||
HeartbeatMessage, | ||
Message, | ||
MessageEmitEvent, | ||
ProtocolMessage, | ||
RequestData, | ||
Status, | ||
WithoutResponse, | ||
} from './types' | ||
import type {HeartbeatMessage, Message, ProtocolMessage} from './types' | ||
@@ -48,4 +51,4 @@ /** | ||
*/ | ||
export type NodeActor<R extends Message, U extends Message> = ActorRefFrom< | ||
ReturnType<typeof createNodeMachine<R, U>> | ||
export type NodeActorLogic<R extends Message, S extends Message> = ReturnType< | ||
typeof createNodeMachine<R, S> | ||
> | ||
@@ -56,2 +59,7 @@ | ||
*/ | ||
export type NodeActor<R extends Message, S extends Message> = ActorRefFrom<NodeActorLogic<R, S>> | ||
/** | ||
* @public | ||
*/ | ||
export type Node<R extends Message, S extends Message> = { | ||
@@ -61,4 +69,5 @@ actor: NodeActor<R, S> | ||
data: U, | ||
) => S extends U ? (S['type'] extends T ? S['response'] : never) : never | ||
machine: ReturnType<typeof createNodeMachine<R, S>> | ||
options?: {signal?: AbortSignal}, | ||
) => S extends U ? (S['type'] extends T ? Promise<S['response']> : never) : never | ||
machine: NodeActorLogic<R, S> | ||
on: <T extends R['type'], U extends Extract<R, {type: T}>>( | ||
@@ -68,3 +77,3 @@ type: T, | ||
) => () => void | ||
onStatus: (handler: (status: string) => void) => () => void | ||
onStatus: (handler: (status: Status) => void) => () => void | ||
post: (data: WithoutResponse<S>) => void | ||
@@ -86,11 +95,30 @@ start: () => () => void | ||
types: {} as { | ||
children: { | ||
'listen for disconnect': 'listen' | ||
'listen for handshake ack': 'listen' | ||
'listen for handshake syn': 'listen' | ||
'listen for heartbeat': 'listen' | ||
'listen for messages': 'listen' | ||
} | ||
context: { | ||
buffer: Array<{data: V; resolvable?: PromiseWithResolvers<S['response']>}> | ||
buffer: Array<{ | ||
data: V | ||
resolvable?: PromiseWithResolvers<S['response']> | ||
signal?: AbortSignal | ||
}> | ||
connectionId: string | null | ||
connectTo: string | ||
domain: string | ||
// The handshake buffer is a workaround to maintain backwards | ||
// compatibility with the Sanity channels package, which may incorrectly | ||
// send buffered messages _before_ it completes the handshake (i.e. | ||
// sends an ack message). It should be removed in the next major. | ||
handshakeBuffer: Array<{ | ||
type: 'message.received' | ||
message: MessageEvent<ProtocolMessage<R>> | ||
}> | ||
name: string | ||
origin: string | null | ||
requests: Array<RequestActorRef<S>> | ||
target: MessageEventSource | undefined | ||
targetOrigin: string | null | ||
} | ||
@@ -106,6 +134,17 @@ emitted: | ||
| {type: 'message.received'; message: MessageEvent<ProtocolMessage<R>>} | ||
| {type: 'post'; data: V; resolvable?: PromiseWithResolvers<S['response']>} | ||
| { | ||
type: 'post' | ||
data: V | ||
resolvable?: PromiseWithResolvers<S['response']> | ||
signal?: AbortSignal | ||
} | ||
| {type: 'request.aborted'; requestId: string} | ||
| {type: 'request.failed'; requestId: string} | ||
| {type: 'request.success'; requestId: string} | ||
| {type: 'request'; data: RequestData<S> | RequestData<S>[]} | ||
| { | ||
type: 'request.success' | ||
requestId: string | ||
response: S['response'] | null | ||
responseTo: string | undefined | ||
} | ||
| {type: 'request'; data: RequestData<S> | RequestData<S>[]} // @todo align with 'post' type | ||
input: NodeInput | ||
@@ -115,5 +154,11 @@ }, | ||
requestMachine: createRequestMachine<S>(), | ||
listen: listenActor, | ||
listen: createListenLogic(), | ||
}, | ||
actions: { | ||
'buffer incoming message': assign({ | ||
handshakeBuffer: ({event, context}) => { | ||
assertEvent(event, 'message.received') | ||
return [...context.handshakeBuffer, event] | ||
}, | ||
}), | ||
'buffer message': enqueueActions(({enqueue}) => { | ||
@@ -123,3 +168,10 @@ enqueue.assign({ | ||
assertEvent(event, 'post') | ||
return [...context.buffer, {data: event.data, resolvable: event.resolvable}] | ||
return [ | ||
...context.buffer, | ||
{ | ||
data: event.data, | ||
resolvable: event.resolvable, | ||
signal: event.signal, | ||
}, | ||
] | ||
}, | ||
@@ -136,3 +188,3 @@ }) | ||
'create request': assign({ | ||
requests: ({context, event, spawn}) => { | ||
requests: ({context, event, self, spawn}) => { | ||
assertEvent(event, 'request') | ||
@@ -150,8 +202,10 @@ const arr = Array.isArray(event.data) ? event.data : [event.data] | ||
from: context.name, | ||
origin: context.origin!, | ||
parentRef: self, | ||
resolvable: request.resolvable, | ||
responseTo: request.responseTo, | ||
sources: context.target!, | ||
targetOrigin: context.targetOrigin!, | ||
to: context.connectTo, | ||
type: request.type, | ||
signal: request.signal, | ||
}, | ||
@@ -188,3 +242,3 @@ }) | ||
type: 'request', | ||
data: context.buffer.map(({data, resolvable}) => ({ | ||
data: context.buffer.map(({data, resolvable, signal}) => ({ | ||
data: data.data, | ||
@@ -194,2 +248,3 @@ type: data.type, | ||
resolvable, | ||
signal, | ||
})), | ||
@@ -207,2 +262,8 @@ })) | ||
}), | ||
'flush handshake buffer': enqueueActions(({context, enqueue}) => { | ||
context.handshakeBuffer.forEach((event) => enqueue.raise(event)) | ||
enqueue.assign({ | ||
handshakeBuffer: [], | ||
}) | ||
}), | ||
'post': raise(({event}) => { | ||
@@ -217,10 +278,10 @@ assertEvent(event, 'post') | ||
resolvable: event.resolvable, | ||
signal: event.signal, | ||
}, | ||
} | ||
}), | ||
'remove request': assign({ | ||
requests: ({context, event}) => { | ||
assertEvent(event, ['request.success', 'request.failed']) | ||
return context.requests.filter((request) => request.id !== event.requestId) | ||
}, | ||
'remove request': enqueueActions(({context, enqueue, event}) => { | ||
assertEvent(event, ['request.success', 'request.failed', 'request.aborted']) | ||
stopChild(event.requestId) | ||
enqueue.assign({requests: context.requests.filter(({id}) => id !== event.requestId)}) | ||
}), | ||
@@ -234,3 +295,2 @@ 'send response': raise(({event}) => { | ||
responseTo: event.message.data.id, | ||
// @todo Do nodes need to support responses? | ||
data: undefined, | ||
@@ -253,3 +313,3 @@ }, | ||
}, | ||
origin: ({event}) => { | ||
targetOrigin: ({event}) => { | ||
assertEvent(event, 'message.received') | ||
@@ -271,6 +331,7 @@ return event.message.origin | ||
domain: input.domain ?? DOMAIN, | ||
handshakeBuffer: [], | ||
name: input.name, | ||
origin: null, | ||
requests: [], | ||
target: undefined, | ||
targetOrigin: null, | ||
}), | ||
@@ -284,2 +345,5 @@ on: { | ||
}, | ||
'request.aborted': { | ||
actions: 'remove request', | ||
}, | ||
}, | ||
@@ -290,5 +354,8 @@ initial: 'idle', | ||
invoke: { | ||
id: 'listen for handshake syn', | ||
src: 'listen', | ||
id: 'listenForHandshakeSyn', | ||
input: listenInputFromContext(MSG_HANDSHAKE_SYN, {count: 1}), | ||
input: listenInputFromContext({ | ||
include: MSG_HANDSHAKE_SYN, | ||
count: 1, | ||
}), | ||
onDone: { | ||
@@ -312,11 +379,15 @@ target: 'handshaking', | ||
{ | ||
id: 'listen for handshake ack', | ||
src: 'listen', | ||
id: 'listenForHandshakeAck', | ||
input: listenInputFromContext(MSG_HANDSHAKE_ACK, {count: 1}), | ||
input: listenInputFromContext({ | ||
include: MSG_HANDSHAKE_ACK, | ||
count: 1, | ||
}), | ||
onDone: 'connected', | ||
}, | ||
{ | ||
id: 'listen for disconnect', | ||
src: 'listen', | ||
id: 'listenForDisconnect', | ||
input: listenInputFromContext([MSG_DISCONNECT], { | ||
input: listenInputFromContext({ | ||
include: MSG_DISCONNECT, | ||
count: 1, | ||
@@ -326,11 +397,21 @@ responseType: 'disconnect', | ||
}, | ||
{ | ||
id: 'listen for messages', | ||
src: 'listen', | ||
input: listenInputFromContext({ | ||
exclude: [MSG_DISCONNECT, MSG_HANDSHAKE_ACK, MSG_HEARTBEAT, MSG_RESPONSE], | ||
}), | ||
}, | ||
], | ||
on: { | ||
request: { | ||
'request': { | ||
actions: 'create request', | ||
}, | ||
post: { | ||
'post': { | ||
actions: 'buffer message', | ||
}, | ||
disconnect: { | ||
'message.received': { | ||
actions: 'buffer incoming message', | ||
}, | ||
'disconnect': { | ||
target: 'idle', | ||
@@ -341,18 +422,24 @@ }, | ||
connected: { | ||
entry: 'flush buffer', | ||
entry: ['flush handshake buffer', 'flush buffer'], | ||
invoke: [ | ||
{ | ||
id: 'listen for messages', | ||
src: 'listen', | ||
id: 'listenForMessages', | ||
input: listenInputFromContext([MSG_RESPONSE, MSG_HEARTBEAT], {matches: false}), | ||
input: listenInputFromContext({ | ||
exclude: [MSG_RESPONSE, MSG_HEARTBEAT], | ||
}), | ||
}, | ||
{ | ||
id: 'listen for heartbeat', | ||
src: 'listen', | ||
id: 'listenForHeartbeats', | ||
input: listenInputFromContext([MSG_HEARTBEAT], {responseType: 'heartbeat.received'}), | ||
input: listenInputFromContext({ | ||
include: MSG_HEARTBEAT, | ||
responseType: 'heartbeat.received', | ||
}), | ||
}, | ||
{ | ||
id: 'listen for disconnect', | ||
src: 'listen', | ||
id: 'listenForDisconnect', | ||
input: listenInputFromContext([MSG_DISCONNECT], { | ||
input: listenInputFromContext({ | ||
include: MSG_DISCONNECT, | ||
count: 1, | ||
@@ -389,5 +476,6 @@ responseType: 'disconnect', | ||
*/ | ||
export const createNode = <R extends Message, S extends Message>(input: NodeInput): Node<R, S> => { | ||
const machine = createNodeMachine<R, S>() | ||
export const createNode = <R extends Message, S extends Message>( | ||
input: NodeInput, | ||
machine: NodeActorLogic<R, S> = createNodeMachine<R, S>(), | ||
): Node<R, S> => { | ||
const actor = createActor(machine, { | ||
@@ -411,9 +499,10 @@ input, | ||
const onStatus = (handler: (status: string) => void) => { | ||
const onStatus = (handler: (status: Status) => void) => { | ||
const snapshot = actor.getSnapshot() | ||
let currentStatus: string | undefined = | ||
let currentStatus: Status = | ||
typeof snapshot.value === 'string' ? snapshot.value : Object.keys(snapshot.value)[0] | ||
const {unsubscribe} = actor.subscribe((state) => { | ||
const status = typeof state.value === 'string' ? state.value : Object.keys(state.value)[0] | ||
const status: Status = | ||
typeof state.value === 'string' ? state.value : Object.keys(state.value)[0] | ||
if (currentStatus !== status) { | ||
@@ -431,5 +520,10 @@ currentStatus = status | ||
const fetch = (data: WithoutResponse<S>) => { | ||
const fetch = (data: WithoutResponse<S>, options?: {signal?: AbortSignal}) => { | ||
const resolvable = Promise.withResolvers<S['response']>() | ||
actor.send({type: 'post', data, resolvable}) | ||
actor.send({ | ||
type: 'post', | ||
data, | ||
resolvable, | ||
signal: options?.signal, | ||
}) | ||
return resolvable.promise as never | ||
@@ -436,0 +530,0 @@ } |
@@ -1,8 +0,24 @@ | ||
import {filter, fromEvent, take} from 'rxjs' | ||
import {EMPTY, filter, fromEvent, map, Observable, take, takeUntil} from 'rxjs' | ||
import {v4 as uuid} from 'uuid' | ||
import {type ActorRefFrom, assign, fromEventObservable, sendParent, setup} from 'xstate' | ||
import { | ||
assign, | ||
fromEventObservable, | ||
sendTo, | ||
setup, | ||
type ActorRefFrom, | ||
type AnyActorRef, | ||
} from 'xstate' | ||
import {MSG_RESPONSE, RESPONSE_TIMEOUT} from './constants' | ||
import type {Message, MessageData, MessageType, ProtocolMessage, ResponseMessage} from './types' | ||
const throwOnEvent = | ||
<T>(message?: string) => | ||
(source: Observable<T>) => | ||
source.pipe( | ||
take(1), | ||
map(() => { | ||
throw new Error(message) | ||
}), | ||
) | ||
/** | ||
@@ -18,7 +34,9 @@ * @public | ||
id: string | ||
origin: string | ||
parentRef: AnyActorRef | ||
resolvable: PromiseWithResolvers<S['response']> | undefined | ||
response: S['response'] | null | ||
responseTo: string | undefined | ||
signal: AbortSignal | undefined | ||
sources: Set<MessageEventSource> | ||
targetOrigin: string | ||
to: string | ||
@@ -44,6 +62,11 @@ type: MessageType | ||
types: {} as { | ||
children: { | ||
'listen for response': 'listen' | ||
} | ||
context: RequestMachineContext<S> | ||
// @todo Should response types be specified? | ||
events: {type: 'message'; data: ProtocolMessage<ResponseMessage>} | ||
events: {type: 'message'; data: ProtocolMessage<ResponseMessage>} | {type: 'abort'} | ||
emitted: | ||
| {type: 'request.failed'; requestId: string} | ||
| {type: 'request.aborted'; requestId: string} | ||
| { | ||
@@ -55,3 +78,2 @@ type: 'request.success' | ||
} | ||
| {type: 'request.failed'; requestId: string} | ||
input: { | ||
@@ -63,23 +85,46 @@ connectionId: string | ||
from: string | ||
origin: string | ||
parentRef: AnyActorRef | ||
resolvable?: PromiseWithResolvers<S['response']> | ||
responseTo?: string | ||
resolvable?: PromiseWithResolvers<S['response']> | ||
signal?: AbortSignal | ||
sources: Set<MessageEventSource> | MessageEventSource | ||
targetOrigin: string | ||
to: string | ||
type: S['type'] | ||
} | ||
output: { | ||
requestId: string | ||
response: S['response'] | null | ||
responseTo: string | undefined | ||
} | ||
}, | ||
actors: { | ||
listen: fromEventObservable( | ||
({input}: {input: {requestId: string; sources: Set<MessageEventSource>}}) => | ||
fromEvent<MessageEvent<ProtocolMessage<ResponseMessage>>>(window, 'message').pipe( | ||
filter( | ||
(event) => | ||
event.data.type === MSG_RESPONSE && | ||
event.data.responseTo === input.requestId && | ||
!!event.source && | ||
input.sources.has(event.source), | ||
), | ||
({ | ||
input, | ||
}: { | ||
input: { | ||
requestId: string | ||
sources: Set<MessageEventSource> | ||
signal?: AbortSignal | ||
} | ||
}) => { | ||
const abortSignal$ = input.signal | ||
? fromEvent(input.signal, 'abort').pipe( | ||
throwOnEvent(`Request ${input.requestId} aborted`), | ||
) | ||
: EMPTY | ||
const messageFilter = (event: MessageEvent<ProtocolMessage<ResponseMessage>>) => | ||
event.data?.type === MSG_RESPONSE && | ||
event.data?.responseTo === input.requestId && | ||
!!event.source && | ||
input.sources.has(event.source) | ||
return fromEvent<MessageEvent<ProtocolMessage<ResponseMessage>>>(window, 'message').pipe( | ||
filter(messageFilter), | ||
take(input.sources.size), | ||
), | ||
takeUntil(abortSignal$), | ||
) | ||
}, | ||
), | ||
@@ -89,28 +134,41 @@ }, | ||
'send message': ({context}, params: {message: ProtocolMessage}) => { | ||
const {sources, origin} = context | ||
const {sources, targetOrigin} = context | ||
const {message} = params | ||
sources.forEach((source) => { | ||
source.postMessage(message, {targetOrigin: origin}) | ||
source.postMessage(message, {targetOrigin}) | ||
}) | ||
}, | ||
'on success': sendParent(({context, self}) => { | ||
if (context.response) { | ||
context.resolvable?.resolve(context.response) | ||
} | ||
return { | ||
type: 'request.success', | ||
requestId: self.id, | ||
response: context.response, | ||
responseTo: context.responseTo, | ||
} | ||
}), | ||
'on fail': sendParent(({context, self}) => { | ||
// eslint-disable-next-line no-console | ||
console.warn( | ||
`Received no response to message '${context.type}' on client '${context.from}' (ID: '${context.id}').`, | ||
) | ||
context.resolvable?.reject(new Error('No response received')) | ||
return {type: 'request.failed', requestId: self.id} | ||
}), | ||
'on success': sendTo( | ||
({context}) => context.parentRef, | ||
({context, self}) => { | ||
if (context.response) { | ||
context.resolvable?.resolve(context.response) | ||
} | ||
return { | ||
type: 'request.success', | ||
requestId: self.id, | ||
response: context.response, | ||
responseTo: context.responseTo, | ||
} | ||
}, | ||
), | ||
'on fail': sendTo( | ||
({context}) => context.parentRef, | ||
({context, self}) => { | ||
// eslint-disable-next-line no-console | ||
console.warn( | ||
`Received no response to message '${context.type}' on client '${context.from}' (ID: '${context.id}').`, | ||
) | ||
context.resolvable?.reject(new Error('No response received')) | ||
return {type: 'request.failed', requestId: self.id} | ||
}, | ||
), | ||
'on abort': sendTo( | ||
({context}) => context.parentRef, | ||
({context, self}) => { | ||
context.resolvable?.reject(new Error('Request aborted')) | ||
return {type: 'request.aborted', requestId: self.id} | ||
}, | ||
), | ||
}, | ||
@@ -125,3 +183,3 @@ guards: { | ||
}).createMachine({ | ||
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOlwgBswBiAD1gBd0GwT0AzFgJ2QNwdzoKAFVyowAewCuDItTRY8hUuSoBtAAwBdRKAAOE2P1wT8ukLUQBGAEwBWEnY3ONAZlc2XATjtWANCAAntYaAGwkLs5ergAsAOxeMVahcQC+qQEKOATEJLBg+BAEUNSaOkggBkYCpuaWCL4aJFYaVnZxABwd7lYxbnEBwQj2TfGhdilJYXE2cTHpmRjZynkFRfglalbl+obGtRX1jc2t7V09fa4DQYheNiQxXk9eHXFxbl7JVgsgWUq56AA7uhjBtqOJYLB0DAyuYqvszIdEF0rA84qFQo87LM4lY8YNEDY8RFIi1bFYvM40hlfkt-qQgSCBGD6EwWGxOGAeFw4AZ8PlROJpLJ5HScgzgaCoLCKvCaojQPUUWiMVicXj-DcEK4NHEHnZXOTQhSbKENDFQukafgJBA4OY-uK4Xt5XVEABaUIEhCekmRf0uamLRTisiUMDO6omBUWRAxGzeloOf1zcZ2C2vH6Olb5QrFSMIt3DLx6ryhV6hGwl3xWDwxb0dVGPZ7vDHvGKGrNilaMqUF11IhC4vVuWsUg2m5IJrU2ewkWfjUJXWI2eN2OxdkMrdggqgQfvRostN4kT5TaK9WwY702Rskd6G1yY3VXGIbmnZ3KwKSYTBweCyi6h6DsepaXhoF5JKaXpaneTguDYrgdI8dbUukQA */ | ||
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOlwgBswBiAD1gBd0GwT0AzFgJ2QNwdzoKAFVyowAewCuDItTRY8hUuSoBtAAwBdRKAAOE2P1wT8ukLUQBGAEwBWEgBYAnK+eOAzB7sB2DzY8rABoQAE9rDQc3V0cNTw8fAA4NHwBfVJCFHAJiElgwfAgCKGpNHSQQAyMBU3NLBDsrDxI7DTaAjQA2OOcNDxDwhHsNJx9Ou0TOq2cJxP9HdMyMbOU8gqL8ErUrcv1DY1qK+sbm1vaPLp6+gcRnGydo9wDGycWQLKVc9AB3dGNN6jiWCwdAwMrmKoHMxHRCJRKOEiJHwuZKBZwXKzBMKIGyYkhtAkXOweTqOHw2RJvD45Ug-P4CAH0JgsNicMA8LhwAz4fKicTSWTyZafWm-f5QcEVSE1aGgepwhFIlF9aYYrGDC4+JzEppjGzOUkeGbpDIgfASCBwczU5QQ-YyuqIAC0nRuCBd+IJXu9KSpwppZEoYDt1RMsosiEcNjdVjiJEeGisiSTHkcVgWpptuXyhWKIahjqGzi1BqRJINnVcdkcbuTLS9VYC8ISfsUAbp4vzDphCHJIyjBvJNlxNmRNexQ3sJGH43GPj8jWJrZWuXYfyoEC7YcLsbrgRsjkcvkmdgNbopVhIPhVfnsh8ClMz-tWsCkmEwcHgUvt257u8v+6Hse4xnhOdZnImVidPqCRNB4JqpEAA */ | ||
context: ({input}) => { | ||
@@ -135,7 +193,9 @@ return { | ||
id: `msg-${uuid()}`, | ||
origin: input.origin, | ||
parentRef: input.parentRef, | ||
resolvable: input.resolvable, | ||
response: null, | ||
responseTo: input.responseTo, | ||
sources: 'size' in input.sources ? input.sources : new Set([input.sources]), | ||
signal: input.signal, | ||
sources: input.sources instanceof Set ? input.sources : new Set([input.sources]), | ||
targetOrigin: input.targetOrigin, | ||
to: input.to, | ||
@@ -146,6 +206,13 @@ type: input.type, | ||
initial: 'idle', | ||
on: { | ||
abort: '.aborted', | ||
}, | ||
states: { | ||
idle: { | ||
after: { | ||
initialTimeout: 'sending', | ||
initialTimeout: [ | ||
{ | ||
target: 'sending', | ||
}, | ||
], | ||
}, | ||
@@ -181,2 +248,3 @@ }, | ||
invoke: { | ||
id: 'listen for response', | ||
src: 'listen', | ||
@@ -186,3 +254,5 @@ input: ({context}) => ({ | ||
sources: context.sources, | ||
signal: context.signal, | ||
}), | ||
onError: 'aborted', | ||
}, | ||
@@ -210,2 +280,6 @@ after: { | ||
}, | ||
aborted: { | ||
type: 'final', | ||
entry: 'on abort', | ||
}, | ||
}, | ||
@@ -212,0 +286,0 @@ output: ({context, self}) => { |
@@ -1,2 +0,9 @@ | ||
import type {MSG_HEARTBEAT, MSG_RESPONSE} from './constants' | ||
import { | ||
MSG_DISCONNECT, | ||
MSG_HANDSHAKE_ACK, | ||
MSG_HANDSHAKE_SYN, | ||
MSG_HANDSHAKE_SYN_ACK, | ||
MSG_HEARTBEAT, | ||
type MSG_RESPONSE, | ||
} from './constants' | ||
@@ -6,2 +13,12 @@ /** | ||
*/ | ||
export type Status = string // @todo strongly type these | ||
/** | ||
* @public | ||
*/ | ||
export type StatusEvent = {channel: string; status: Status} | ||
/** | ||
* @public | ||
*/ | ||
export type MessageType = string | ||
@@ -32,2 +49,3 @@ | ||
resolvable?: PromiseWithResolvers<S['response']> | ||
signal?: AbortSignal | ||
} | ||
@@ -44,10 +62,10 @@ | ||
export interface ListenInput { | ||
count?: number | ||
domain: string | ||
exclude: string[] | ||
from: string | ||
to: string | ||
type: string | string[] | ||
matches: boolean | ||
count?: number | ||
include: string[] | ||
responseType: string | ||
target: MessageEventSource | undefined | ||
to: string | ||
} | ||
@@ -123,1 +141,28 @@ | ||
} | ||
/** | ||
* @internal | ||
*/ | ||
export interface DisconnectMessage { | ||
type: typeof MSG_DISCONNECT | ||
data: undefined | ||
} | ||
/** | ||
* @internal | ||
*/ | ||
export type HandshakeMessageType = | ||
| typeof MSG_HANDSHAKE_ACK | ||
| typeof MSG_HANDSHAKE_SYN | ||
| typeof MSG_HANDSHAKE_SYN_ACK | ||
/** | ||
* @internal | ||
*/ | ||
export type InternalMessageType = | ||
| typeof MSG_DISCONNECT | ||
| typeof MSG_HANDSHAKE_ACK | ||
| typeof MSG_HANDSHAKE_SYN | ||
| typeof MSG_HANDSHAKE_SYN_ACK | ||
| typeof MSG_HEARTBEAT | ||
| typeof MSG_RESPONSE |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
705761
5
8738