@transitive-sdk/utils-web 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/hooks.jsx +79 -64
- package/client/react-web-component/index.js +12 -0
- package/client/shared.jsx +84 -20
- package/dist/utils-web.js +1530 -1
- package/dist/utils-web.js.map +4 -4
- package/docs/client.md +60 -19
- package/esbuild.js +3 -3
- package/package.json +1 -1
package/dist/utils-web.js
CHANGED
|
@@ -1 +1,1530 @@
|
|
|
1
|
-
var Xe=Object.create;var K=Object.defineProperty;var Ze=Object.getOwnPropertyDescriptor;var Re=Object.getOwnPropertyNames;var et=Object.getPrototypeOf,tt=Object.prototype.hasOwnProperty;var v=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),ge=(t,e)=>{for(var s in e)K(t,s,{get:e[s],enumerable:!0})},G=(t,e,s,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of Re(e))!tt.call(t,o)&&o!==s&&K(t,o,{get:()=>e[o],enumerable:!(r=Ze(e,o))||r.enumerable});return t},y=(t,e,s)=>(G(t,e,"default"),s&&G(s,e,"default")),w=(t,e,s)=>(s=t!=null?Xe(et(t)):{},G(e||!t||!t.__esModule?K(s,"default",{value:t,enumerable:!0}):s,t)),st=t=>G(K({},"__esModule",{value:!0}),t);var pe=v((Rt,be)=>{be.exports=()=>{try{return require("react-web-component-style-loader/exports").styleElements}catch{return[]}}});var ye=v((es,me)=>{me.exports=function(e){if(!e.attributes)return{};let s={},r,n=[...e.attributes].map(c=>({[c.name]:c.value}));for(r of n){let c=Object.keys(r)[0],i=c.replace(/-([a-z])/g,a=>a[1].toUpperCase());s[i]=r[c]}return s}});var qe=v((ts,$e)=>{var rt=require("react"),nt=require("react-dom"),ot=require("react-shadow-dom-retarget-events"),it=pe(),ct=ye(),at={attachedCallback:"webComponentAttached",connectedCallback:"webComponentConnected",disconnectedCallback:"webComponentDisconnected",adoptedCallback:"webComponentAdopted"};$e.exports={create:(t,e,s=!0,r=void 0)=>{let o=class extends HTMLElement{instance=null;callConstructorHook(){this.instance.webComponentConstructed&&this.instance.webComponentConstructed.apply(this.instance,[this])}callLifeCycleHook(n,c=[]){let i=at[n];i&&this.instance&&this.instance[i]&&this.instance[i].apply(this.instance,c)}connectedCallback(){let n=this,c=n;if(s){let i=n.attachShadow({mode:"open"});c=document.createElement("div"),it().forEach(h=>{i.appendChild(h.cloneNode(i))}),i.appendChild(c),ot(i)}nt.render(rt.createElement(t,ct(n)),c,function(){n.instance=this,n.callConstructorHook(),n.callLifeCycleHook("connectedCallback")})}disconnectedCallback(){this.callLifeCycleHook("disconnectedCallback")}adoptedCallback(n,c){this.callLifeCycleHook("adoptedCallback",[n,c])}call(n,c){return r?.current?.[n]?.call(r?.current,c)}getConfig(){return this.instance.state.config}};return customElements.define(e,o),o}}});var Ce=v((rs,Te)=>{Te.exports={rosReleases:{kinetic:{rosVersion:1,ubuntuCodename:"xenial"},melodic:{rosVersion:1,ubuntuCodename:"bionic"},noetic:{rosVersion:1,ubuntuCodename:"focal"},dashing:{rosVersion:2},eloquent:{rosVersion:2},foxy:{rosVersion:2},galactic:{rosVersion:2},humble:{rosVersion:2},iron:{rosVersion:2},rolling:{rosVersion:2}}}});var X=v((ns,Le)=>{var ut=require("semver/functions/compare"),Ee=require("semver/ranges/min-version"),L={get:require("lodash/get"),set:require("lodash/set"),unset:require("lodash/unset"),forEach:require("lodash/forEach"),map:require("lodash/map"),isEmpty:require("lodash/isEmpty"),eq:require("lodash/isEqual"),isPlainObject:require("lodash/isPlainObject"),merge:require("lodash/merge")},B=require("loglevel"),W=require("chalk"),lt=Ce();B.setAll=t=>Object.values(B.getLoggers()).forEach(e=>e.setLevel(t));var Se={warn:W.yellow,error:W.red,info:W.green,debug:W.gray},ht=t=>Se[t]?Se[t](t):t,dt=B.methodFactory;B.methodFactory=(t,e,s)=>{let r=dt(t,e,s);if(typeof window<"u"){let n=`${s} ${t}`;return(...c)=>r(`[${n}]`,...c)}let o=`${s} ${ht(t)}`;return(...n)=>r(`[${W.blue(new Date().toISOString())} ${o}]`,...n)};var ft=B.getLogger,gt=t=>JSON.parse(JSON.stringify(t)),bt=t=>JSON.parse(atob(t.split(".")[1])),pt=t=>{try{return JSON.parse(t)}catch{return null}},we=(t,e,s)=>{t&&(s(t),t[e]?.forEach(r=>we(r,e,s)))},xe=(t,e,s,r=[])=>{s(t,r);let o=e[0];if(o){let n=t[o];n&&xe(n,e.slice(1),s,r.concat(o))}},mt=t=>new Promise(e=>{setTimeout(e,t)}),Oe=(t,e=[],s={})=>(L.forEach(t,(r,o)=>{let n=e.concat(String(o));(L.isPlainObject(r)||r instanceof Array)&&r!==null?Oe(r,n,s):s[De(n)]=r}),s),Pe=(t,e,s,r=[],o={})=>{if(e.length==0||e[0]=="#"){s(t,r,o);return}let n=e[0];if(n){for(let c in t)if(c==n||n=="*"||n.startsWith("+")){let i=n.startsWith("+")&&n.length>1?Object.assign({},o,{[n.slice(1)]:c}):o;Pe(t[c],e.slice(1),s,r.concat([c]),i)}}},ve=(t,e,s)=>{if(e.length==0)return t;let r=e.shift();e.length==0?t[r]=s:(t[r]||(t[r]={}),ve(t[r],e,s))},Me=t=>t.replace(/%/g,"%25").replace(/\//g,"%2F"),_e=t=>t.replace(/%25/g,"%").replace(/%2F/g,"/"),De=t=>{let e=s=>s.startsWith("+")?"+":s;return`/${t.map(e).map(Me).join("/")}`},M=t=>{let e=t.split("/").map(_e);return e.length>0&&e[0].length==0&&e.shift(),e.length>0&&e.at(-1).length==0&&e.pop(),e},ke=(t,e)=>{let s=(n,c)=>{if(n.length==0||n[0][0]=="#"||c.length==0)return!0;if(n[0]==c[0])return s(n.slice(1),c.slice(1));if(n[0][0]=="+"){let i=s(n.slice(1),c.slice(1));return i&&Object.assign({[n[0].slice(1)]:c[0]},i)}return!1},r=M(t),o=M(e);return s(r,o)},yt=(t,e)=>{let s=M(e),r=M(t);return Fe(s,r)&&s.length<r.length},Fe=(t,e)=>t.length==0?!0:t[0]==e[0]&&Fe(t.slice(1),e.slice(1)),$t=t=>{let e=t.split(":");return{organization:e[0],client:e[1],sub:e.slice(2)}},qt=t=>{let e=M(t);return{organization:e[0],device:e[1],capabilityScope:e[2],capabilityName:e[3],capability:`${e[2]}/${e[3]}`,version:e[4],sub:e.slice(5)}},Tt=t=>t.length==0?null:JSON.parse(t.toString("utf-8")),Ct=(t,e,s,r=1e3)=>{let o=[],n=i=>{e.forEach(a=>ke(`${a}/#`,i)&&o.push(i))};t.on("message",n),e.forEach(i=>{typeof i=="string"?t.subscribe(`${i}/#`):console.warn("Ignoring",i,"since it is not a string.")});let c=typeof Buffer<"u"?Buffer.alloc(0):null;setTimeout(()=>{t.removeListener("message",n),e.forEach(a=>t.unsubscribe(`${a}/#`));let i=o.length;console.log(`clearing ${i} retained messages from ${e}`),o.forEach(a=>{t.publish(a,c,{retain:!0})}),s&&s(i)},r)},Et=()=>Math.random().toString(36).slice(2),Y=(t,e)=>ut(Ee(t),Ee(e)),St=(t,e=void 0,s={})=>{if(!t)return e?L.set({},e,t):t;let r=Object.keys(t).filter(c=>(!s.maxVersion||Y(c,s.maxVersion)<=0)&&(!s.minVersion||Y(s.minVersion,c)<=0)).sort(Y),o={},n=e&&M(e);return r.forEach(c=>{let i=n?L.get(t[c],n):t[c];L.merge(o,i)}),n?L.set({},n,o):o},wt=["B","KB","MB","GB","TB"],xt=t=>{if(!t)return"--";let e=0;for(;t>1024;)t/=1024,e++;return`${t.toFixed(2)} ${wt[e]}`},Ot=t=>{if(!t)return"--";let e={};t>3600&&(e.h=Math.floor(t/3600),t=t%3600),t>60&&(e.m=Math.floor(t/60),t=t%60),e.s=Math.floor(t);let s="";return e.h>0&&(s+=`${e.h}h `),e.m>0&&(s+=`${e.m}m `),!e.h&&(s+=`${e.s}s`),s.trim()};Le.exports={parseMQTTUsername:$t,parseMQTTTopic:qt,pathToTopic:De,topicToPath:M,toFlatObject:Oe,topicMatch:ke,mqttParsePayload:Tt,getRandomId:Et,versionCompare:Y,loglevel:B,getLogger:ft,mergeVersions:St,mqttClearRetained:Ct,isSubTopicOf:yt,clone:gt,setFromPath:ve,forMatchIterator:Pe,encodeTopicElement:Me,decodeTopicElement:_e,constants:lt,visit:we,wait:mt,formatBytes:xt,formatDuration:Ot,tryJSONParse:pt,decodeJWT:bt,visitAncestor:xe}});var se=v((os,Ve)=>{var T={get:require("lodash/get"),set:require("lodash/set"),unset:require("lodash/unset"),forEach:require("lodash/forEach"),map:require("lodash/map"),isEmpty:require("lodash/isEmpty"),eq:require("lodash/isEqual"),isPlainObject:require("lodash/isPlainObject"),merge:require("lodash/merge")},{topicToPath:I,pathToTopic:Pt,toFlatObject:vt,topicMatch:Be,forMatchIterator:Mt}=X(),te=(t,e)=>{if(!e||e.length==0)return;T.unset(t,e);let s=e.slice(0,-1),r=s.length==0?t:T.get(t,s);return T.isEmpty(r)?te(t,s):e},_t=(t,e)=>(T.forEach(e,(s,r)=>{let o=I(r);s==null?te(t,o):T.set(t,o,s)}),t),Ae=(t,e)=>{if(e.length==0)return;let s=e[0];if(s)for(let r in t)r!=s&&s!="*"&&!s.startsWith("+")?delete t[r]:Ae(t[r],e.slice(1))},ee=class{#e={};#t=[];#s=[];constructor(e={}){this.#e=e}updateFromArray(e,s,r={}){let o=T.get(this.#e,e);if(s==null){if(o==null)return{};te(this.#e,e)}else{if(T.eq(o,s))return{};T.set(this.#e,e,s)}let n=Pt(e),c={[n]:s},i;if(s instanceof Object){let a=vt(s);i={},T.forEach(a,(h,d)=>{i[`${n}${d}`]=h})}else i=c;return this.#t.forEach(a=>a(c,r)),this.#s.forEach(a=>a(i,r)),i}update(e,s,r){if(typeof e=="string")return this.updateFromTopic(e,s,r);if(e instanceof Array)return this.updateFromArray(e,s,r);throw new Error("unrecognized path expression")}updateFromTopic(e,s,r){return this.updateFromArray(I(e),s,r)}updateFromModifier(e,s){return T.map(e,(r,o)=>this.updateFromTopic(o,r,s))}subscribe(e){e instanceof Function?this.#t.push(e):console.warn("DataCache.subscribe expects a function as argument. Did you mean to use subscribePath?")}subscribePath(e,s){this.#t.push((r,o)=>{T.forEach(r,(n,c)=>{let i=Be(e,c);i&&s(n,c,i,o)})})}subscribePathFlat(e,s){this.#s.push((r,o)=>{T.forEach(r,(n,c)=>{let i=Be(e,c);i&&s(n,c,i,o)})})}unsubscribe(e){this.#t=this.#t.filter(s=>s!=e)}get(e=[]){return e.length==0?this.#e:T.get(this.#e,e)}getByTopic(e){return this.get(I(e))}filter(e){let s=JSON.parse(JSON.stringify(this.get()));return Ae(s,e),s}filterByTopic(e){return this.filter(I(e))}forMatch(e,s){let r=I(e);this.forPathMatch(r,s)}forPathMatch(e,s){Mt(this.get(),e,s)}};Ve.exports={DataCache:ee,updateObject:_t}});var ce=v((cs,Qe)=>{"use strict";var{mqttParsePayload:Dt,topicMatch:re,topicToPath:x,pathToTopic:U,toFlatObject:ne,getLogger:kt,mergeVersions:Ft,parseMQTTTopic:Lt,isSubTopicOf:is,versionCompare:Bt,encodeTopicElement:At,visitAncestor:Vt}=X(),{DataCache:Ne}=se(),O=require("lodash"),p=kt("MqttSync"),oe="$SYS/broker/uptime",A="$_",Nt=()=>{},Ht=t=>typeof t=="object"?JSON.parse(JSON.stringify(t)):t,He=t=>t.endsWith("/#")?t:t.endsWith("/")?t.concat("#"):t.concat("/#"),Je=t=>t.replace(/\/\//g,"/"),ie=class{data=new Ne;subscribedPaths={};publishedPaths={};publishedMessages=new Ne;publishQueue=new Map;heartbeatWaitersOnce=[];heartbeats=0;beforeDisconnectHooks=[];constructor({mqttClient:e,onChange:s,ignoreRetain:r,migrate:o,onReady:n,sliceTopic:c,onHeartbeatGranted:i}){this.mqtt=e,this.mqtt.on("message",(a,h,d)=>{let u=h&&h.toString();if(p.debug("got message",a,u.slice(0,180),u.length>180?`... (${u.length} bytes)`:"",d.retain),a==oe)this.heartbeats>0&&(this.heartbeatWaitersOnce.forEach(f=>f()),this.heartbeatWaitersOnce=[]),this.heartbeats==1&&!o&&n&&n(),this.heartbeats++;else if(d.retain||r){let f=x(a);p.debug("processing message",a,f),c&&(f=f.slice(c),a=U(f));let b=Dt(h);if(this.isPublished(a))this.publishedMessages.updateFromArray([...f,A],b),this.data.update(a,b,{external:!0});else if(this.isSubscribed(a)){p.debug("applying received update",a);let m=this.data.update(a,b);s&&Object.keys(m).length>0&&s(m)}}}),this.mqtt.subscribe(oe,{rap:!0},(a,h)=>{p.debug(oe,{granted:h}),h&&h.length>0&&i?.()}),o?.length>0&&this.migrate(o,()=>{p.debug("done migrating",n),n&&this.waitForHeartbeatOnce(n)})}publishAtLevel(e,s,r){p.debug(`publishingAtLevel ${r}`,e,s),r>0?O.forEach(s,(o,n)=>{let c=`${e}/${At(n)}`;p.debug(`publishing ${c}`),this.publishAtLevel(c,o,r-1)}):this.mqtt.publish(e,JSON.stringify(s),{retain:!0},o=>{o&&p.warn("Error when publishing migration result",o)})}migrate(e,s=void 0){let r=e.length;if(r==0){s&&s();return}let o=()=>--r==0&&s&&s();e.forEach(({topic:n,newVersion:c,transform:i=void 0,flat:a=!1,level:h=0})=>{p.debug("migrating",n,c);let{organization:d,device:u,capability:f,sub:b}=Lt(n),m=`/${d}/${u}/${f}`,C=b.length==0?"/#":U(b),E=`${m}/+${C}`;this.subscribe(E,D=>{if(D){p.warn("Error during migration",D),o();return}this.waitForHeartbeatOnce(()=>{p.debug("got heartbeat",n,E);let H=this.data.getByTopic(m);if(!H){this.unsubscribe(E),o();return}let j=Ft(H,C,{maxVersion:c}),k=O.get(j,x(C)),F=i?i(k):k,J=Je(`${m}/${c}/${C}`);if(p.debug("publishing merged",J),a){let S=ne(F),P=x(J);O.forEach(S,(Q,de)=>{let Ye=U(P.concat(x(de)));this.mqtt.publish(Ye,JSON.stringify(Q),{retain:!0},fe=>{fe&&p.warn(`Error when publishing migration result for ${de}`,fe)})})}else this.publishAtLevel(J,F,h);this.unsubscribe(E),this.waitForHeartbeatOnce(()=>{let P=Object.keys(H).filter(Q=>Bt(Q,c)<0).map(Q=>Je(`${m}/${Q}/${C}`));p.debug({prefixesToClear:P}),this.clear(P),o()})})})})}clear(e,s=void 0,r={}){let o=[],n=i=>{e.forEach(a=>re(`${a}/#`,i)&&(!r.filter||r.filter(i))&&o.push(i))};this.mqtt.on("message",n),e.forEach(i=>{typeof i=="string"?this.mqtt.subscribe(`${i}/#`):p.warn("Ignoring",i,"since it is not a string.")});let c=typeof Buffer<"u"?Buffer.alloc(0):null;this.waitForHeartbeatOnce(()=>{this.mqtt.removeListener("message",n),e.forEach(a=>this.mqtt.unsubscribe(`${a}/#`));let i=o.length;p.debug(`clearing ${i} retained messages from ${e}`),o.forEach(a=>{this.mqtt.publish(a,c,{retain:!0})}),s&&s(i)})}waitForHeartbeatOnce(e){setTimeout(()=>this.heartbeatWaitersOnce.push(e),1)}isSubscribed(e){return Object.keys(this.subscribedPaths).some(s=>re(s,e))}isPublished(e){return Object.keys(this.publishedPaths).some(s=>re(s,e)&&!this.publishedPaths[s].atomic)}subscribe(e,s=Nt){if(e=He(e),p.debug("subscribing to",e),this.subscribedPaths[e]){p.debug("already subscribed to",e),s();return}this.mqtt.subscribe(e,{rap:!0},(r,o)=>{p.debug("subscribe",e,"granted:",o),o&&o.some(n=>n.topic==e&&n.qos<128)?(this.subscribedPaths[e]=1,s(null)):s(`not permitted to subscribe to topic ${e}, ${JSON.stringify(o)}`)})}unsubscribe(e){this.subscribedPaths[e]&&(this.mqtt.unsubscribe(e),delete this.subscribedPaths[e])}_actuallyPublish(e,s){return this.mqtt.connected?(p.debug("actually publishing",e),this.mqtt.publish(e,s==null?null:JSON.stringify(s),{retain:!0}),!0):(p.warn("not connected, not publishing",e),!1)}_processQueue_rec(e){if(this.publishQueue.size>0){let[s,r]=this.publishQueue.entries().next().value;this._actuallyPublish(s,r)?(this.publishQueue.delete(s),this._processQueue_rec(e)):setTimeout(()=>this._processQueue_rec(e),5e3)}else e()}_processQueue(){this._processing||(this._processing=!0,this._processQueue_rec(()=>this._processing=!1))}setThrottle(e){this._processQueueThrottled=O.throttle(this._processQueue.bind(this),e)}clearThrottle(){delete this._processQueueThrottled}addToQueue(e,s){this.publishQueue.set(e,s)}_enqueue(e,s){p.debug("enqueuing",e),this.addToQueue(e,s),this._processQueueThrottled?this._processQueueThrottled():this._processQueue();let r=x(e);this.publishedMessages.updateFromArray([...r,A],s==null?null:Ht(s))}publish(e,s={atomic:!1}){if(e=He(e),O.isEqual(this.publishedPaths[e],s))return!1;if(this.publishedPaths[e]=s,s.atomic)return this.data.subscribePath(e,(r,o,n,c)=>{if(c?.external)return;p.debug("processing change (atomic)",o,e);let i=e.slice(0,e.length-2),a=U(x(o).slice(0,x(i).length));this._enqueue(a,this.data.getByTopic(a))}),!0;this.mqtt.subscribe(e),this.data.subscribePath(e,(r,o,n,c)=>{if(c?.external)return;p.debug("processing change",o);let i=x(o),a=this.publishedMessages.get(i);O.each(a,(d,u)=>{if(u==A)return!0;let f=Object.keys(ne(d)).filter(b=>b.endsWith(A));p.debug("flat->atomic: ",{toClear:f},u),f.forEach(b=>{let m=b.slice(0,-(A.length+1)),C=`${o}/${u}/${m}`;this._enqueue(C,null)})});let h=this.publishedMessages.get();return Vt(h,i.slice(0,-1),(d,u)=>{let f=d[A];if(f&&O.isObject(f)){p.debug("atomic->flat",{oldVal:f});let b=U(u);this._enqueue(b,null);let m=ne(f);O.each(m,(C,E)=>{let D=`${b}${E}`;this._enqueue(D,C)})}}),this._enqueue(o,r),!0})}beforeDisconnect(){this.beforeDisconnectHooks.forEach(e=>e(this))}onBeforeDisconnect(e){this.beforeDisconnectHooks.push(e)}};Qe.exports=ie});var N={};ge(N,{Code:()=>zt,ErrorBoundary:()=>he,InlineCode:()=>jt,LevelBadge:()=>Ut,MqttSync:()=>Ie,Timer:()=>Gt,TimerContext:()=>Ke,TransitiveCapability:()=>Kt,createWebComponent:()=>Xt,fetchJson:()=>ze,parseCookie:()=>Ue,useCapability:()=>le,useMqttSync:()=>ue,useTopics:()=>Wt,useTransitive:()=>Qt});module.exports=st(N);var g=w(require("react")),_=require("react-bootstrap"),Ge=w(qe());var l={};ge(l,{MqttSync:()=>Ie,fetchJson:()=>ze,parseCookie:()=>Ue});y(l,w(X()));y(l,w(se()));var We=w(ce()),Ie=We.default,Ue=t=>t.split(";").map(e=>e.split("=")).reduce((e,s)=>(e[decodeURIComponent(s[0].trim())]=s[1]&&decodeURIComponent(s[1].trim()),e),{}),ze=(t,e,s={})=>{fetch(t,{method:s.method||(s.body?"post":"get"),mode:"cors",cache:"no-cache",headers:{"Content-Type":"application/json",...s.headers},redirect:"follow",referrerPolicy:"no-referrer",body:s.body?JSON.stringify(s.body):void 0}).then(r=>{let o=!r.ok&&`fetching ${t} failed: ${r.status} ${r.statusText}`;r.json().then(n=>e(o,n)).catch(n=>{throw new Error(n)})}).catch(r=>e(`error: ${r}`))};var q=w(require("react")),R=w(require("lodash")),je=w(require("mqtt-browser"));var Jt=ce(),$=(0,l.getLogger)("utils-web/hooks");$.setLevel("info");$.setLevel("debug");var ue=({jwt:t,id:e,mqttUrl:s})=>{let[r,o]=(0,q.useState)("connecting"),[n,c]=(0,q.useState)(),[i,a]=(0,q.useState)({}),[h,d]=(0,q.useState)(!1);return(0,q.useEffect)(()=>{let u=(0,l.decodeJWT)(t),f=je.default.connect(s,{username:JSON.stringify({id:e,payload:u}),password:t});return f.on("connect",()=>{$.debug("connected");let b=new Jt({mqttClient:f,ignoreRetain:!0,onHeartbeatGranted:()=>d(!0)});c(b),o("connected"),b.data.subscribe(R.default.throttle(()=>a((0,l.clone)(b.data.get())),50))}),f.on("error",b=>{$.error(b),o(`error: ${b}`)}),()=>{$.info("cleaning up useMQTTSync"),n&&n.beforeDisconnect?(n.beforeDisconnect(),n.waitForHeartbeatOnce(()=>f.end())):f.end()}},[t,e]),{status:r,ready:h,StatusComponent:()=>q.default.createElement("div",null,r),mqttSync:n,data:i}},Qt=({jwt:t,id:e,host:s,ssl:r,capability:o,versionNS:n})=>{let[c,i]=o.split("/"),{device:a}=(0,l.decodeJWT)(t),h=[e,a,c,i],d=(0,l.pathToTopic)(h),u=[...h,n],f=(0,l.pathToTopic)(u),b=`${r&&JSON.parse(r)?"wss":"ws"}://mqtt.${s}`;return{...ue({jwt:t,id:e,mqttUrl:b}),device:a,prefixPath:h,prefix:d,prefixPathVersion:u,prefixVersion:f}},Wt=({jwt:t,host:e="transitiverobotics.com",ssl:s=!0,topics:r=[]})=>{let[o,n]=(0,q.useState)();!R.default.isEqual(o,r)&&n(r);let{device:c,id:i,capability:a}=(0,l.decodeJWT)(t);if(c=="_fleet"){$.warn("useTopics only works for device JWTs, not _fleet ones");return}let h=`/${i}/${c}/@transitive-robotics/_robot-agent/+/status`,{mqttSync:d,data:u,status:f,ready:b,StatusComponent:m}=ue({jwt:t,id:i,mqttUrl:`ws${s?"s":""}://mqtt.${e}`});(0,q.useEffect)(()=>{b&&d.subscribe(h,S=>S&&console.warn(S))},[d,b]);let C=(0,l.mergeVersions)(u[i]?.[c]["@transitive-robotics"]["_robot-agent"],"status").status,E=C?.runningPackages,[D,H]=a.split("/"),j=E?.[D]?.[H],k=j&&Object.values(j).filter(Boolean)[0],F=`/${i}/${c}/${a}/${k}`;(0,q.useEffect)(()=>{$.debug("topics",r),k&&r.forEach(S=>{$.debug(`subscribing to ${F}${S}`),d.subscribe(`${F}${S}`,P=>P&&$.warn(P))})},[o,k,d]);let J=R.default.get(u,(0,l.topicToPath)(F));return{data:u?.[i]?.[c],mqttSync:d,agentStatus:C,topicData:J}},Z={},ae={},le=({capability:t,name:e,userId:s,deviceId:r,host:o="transitiverobotics.com",ssl:n=!0})=>{let[c,i]=(0,q.useState)({loaded:!1}),a=(d,u)=>{$.debug(`custom component ${e}: ${d}`),ae[e]=u,i(f=>({...f,loadedModule:u,loaded:!!u}))},h=(...d)=>Z[e].forEach(u=>u(...d));return(0,q.useEffect)(()=>{if($.debug(`loading custom component ${e}`),ae[e])return a("already loaded",ae[e]);if(Z[e])return $.debug("already loading"),Z[e].push(a);Z[e]=[a];let d=`http${n?"s":""}://portal.${o}`,u=new URLSearchParams({userId:s,deviceId:r}),f=`${d}/running/${t}/dist/${e}`;import(`${f}.esm.js?${u.toString()}`).then(b=>h("loaded esm",b),b=>{$.warn(`No ESM module found for ${e}, loading iife`),import(`${f}.js?${u.toString()}`).then(m=>h("loaded iife",m),m=>$.error(`Failed to load ${e} iife`,m))})},[t,e,s,r]),c};var V={badge:{width:"4em"},code:{color:"#700",borderLeft:"3px solid #aaa",padding:"0.5em 0px 0.5em 2em",backgroundColor:"#f0f0f0",borderRadius:"4px",marginTop:"0.5em"},inlineCode:{color:"#700",margin:"0px 0.5em 0px 0.5em"}},It=[g.default.createElement(_.Badge,{bg:"success",style:V.badge},"OK"),g.default.createElement(_.Badge,{bg:"warning",style:V.badge},"Warn"),g.default.createElement(_.Badge,{bg:"danger",style:V.badge},"Error"),g.default.createElement(_.Badge,{bg:"secondary",style:V.badge},"Stale")],Ut=({level:t})=>It[t]||g.default.createElement("span",null,t),zt=({children:t})=>g.default.createElement("pre",{style:V.code},t),jt=({children:t})=>g.default.createElement("tt",{style:V.inlineCode},t),z={},Ke=g.default.createContext({}),Gt=({duration:t,onTimeout:e,onStart:s,setOnDisconnect:r,children:o})=>{t=t||60;let[n,c]=(0,g.useState)(t),[i,a]=(0,g.useState)(!1),h=(0,g.useMemo)(()=>Math.random().toString(36).slice(2),[]),d=()=>{console.log("stopping timer for",h),e&&setTimeout(e,1),clearInterval(z[h]),z[h]=null,a(!1)},u=()=>{let b=z[h];return console.log(b,z,n),!b&&n>0&&(a(!0),z[h]=setInterval(()=>c(m=>{if(--m>0)return m;d()}),1e3),s&&setTimeout(s,1)),d};(0,g.useEffect)(()=>{n>0&&!i&&u()},[n]),(0,g.useEffect)(()=>d,[]),r&&r(()=>{d()});let f=()=>c(t);return g.default.createElement(Ke.Provider,{value:{reset:f,duration:t,timer:n}},n>0?g.default.createElement("div",null,o,n<60&&g.default.createElement("div",null,"Timeout in: ",n," seconds")):g.default.createElement("div",null,"Timed out. ",g.default.createElement(_.Button,{onClick:f},"Resume")))},Kt=({jwt:t,host:e="transitiverobotics.com",ssl:s=!0,...r})=>{let{id:o,device:n,capability:c}=(0,l.decodeJWT)(t),i=n=="_fleet"?"fleet":"device",h=`${c.split("/")[1]}-${i}`,{loaded:d}=le({capability:c,name:h,userId:o||r.userId,deviceId:n,host:e,ssl:s}),u=(0,g.useRef)();(0,g.useEffect)(()=>{u.current?.instance?.setState(b=>({...b,id:o,jwt:t,host:e,ssl:s,...r}))},[u.current,o,t,e,s,...Object.values(r)]);let f=(0,g.useMemo)(()=>({id:o,jwt:t,host:e,ssl:s,...r}),[]);return d?g.default.createElement(h,{...f,ref:u}):g.default.createElement("div",null,"Loading ",h)},he=class extends g.default.Component{constructor(e){super(e),this.state={hasError:!1}}static getDerivedStateFromError(e){return{hasError:!0}}componentDidCatch(e,s){console.warn("ErrorBoundary caught:",e,s)}render(){return this.state.hasError?g.default.createElement("div",null,this.props.message||"Something went wrong here."):this.props.children}},Yt=t=>t.$$typeof==Symbol.for("react.forward_ref")||t.prototype?.render,Xt=(t,e,s="0.0.0",r={})=>{let o=Yt(t)?g.default.createRef():null;class n extends g.default.Component{onDisconnect=null;state={};setOnDisconnect(i){this.onDisconnect=i}webComponentConstructed(i){let a=new MutationObserver(h=>{let d={};h.forEach(({attributeName:u})=>{d[u]=i.getAttribute(u)}),this.setState(u=>({...u,...d}))}).observe(i,{attributes:!0})}webComponentDisconnected(){this.setState({_disconnected:!0});try{this.onDisconnect&&this.onDisconnect()}catch(i){console.log("Error during onDisconnect of web-component",i)}}setConfig(i){this.setState({config:i})}render(){let i=r.stylesheets||["https://cdn.jsdelivr.net/gh/transitiverobotics/transitive-utils@0.8.3/web/css/bootstrap_transitive-bs-root.min.css"];return g.default.createElement("div",{id:`cap-${e}-${s}`,className:r.className||"transitive-bs-root"},g.default.createElement("style",null,i.map(a=>`@import url(${a});`)),!this.state._disconnected&&g.default.createElement(t,{ref:o,...this.props,...this.state,setOnDisconnect:this.setOnDisconnect.bind(this),setConfig:this.setConfig.bind(this)}))}}return Ge.default.create(n,e,r.shadowDOM||!1,o)};y(N,l,module.exports);
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// client/react-web-component/getStyleElementsFromReactWebComponentStyleLoader.js
|
|
34
|
+
var require_getStyleElementsFromReactWebComponentStyleLoader = __commonJS({
|
|
35
|
+
"client/react-web-component/getStyleElementsFromReactWebComponentStyleLoader.js"(exports, module2) {
|
|
36
|
+
module2.exports = () => {
|
|
37
|
+
try {
|
|
38
|
+
return require("react-web-component-style-loader/exports").styleElements;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// client/react-web-component/extractAttributes.js
|
|
47
|
+
var require_extractAttributes = __commonJS({
|
|
48
|
+
"client/react-web-component/extractAttributes.js"(exports, module2) {
|
|
49
|
+
module2.exports = function extractAttributes(nodeMap) {
|
|
50
|
+
if (!nodeMap.attributes) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
let obj = {};
|
|
54
|
+
let attribute;
|
|
55
|
+
const attributesAsNodeMap = [...nodeMap.attributes];
|
|
56
|
+
const attributes = attributesAsNodeMap.map((attribute2) => ({ [attribute2.name]: attribute2.value }));
|
|
57
|
+
for (attribute of attributes) {
|
|
58
|
+
const key = Object.keys(attribute)[0];
|
|
59
|
+
const camelCasedKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
60
|
+
obj[camelCasedKey] = attribute[key];
|
|
61
|
+
}
|
|
62
|
+
return obj;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// client/react-web-component/index.js
|
|
68
|
+
var require_react_web_component = __commonJS({
|
|
69
|
+
"client/react-web-component/index.js"(exports, module2) {
|
|
70
|
+
var React3 = require("react");
|
|
71
|
+
var ReactDOM = require("react-dom");
|
|
72
|
+
var retargetEvents = require("react-shadow-dom-retarget-events");
|
|
73
|
+
var getStyleElementsFromReactWebComponentStyleLoader = require_getStyleElementsFromReactWebComponentStyleLoader();
|
|
74
|
+
var extractAttributes = require_extractAttributes();
|
|
75
|
+
var lifeCycleHooks = {
|
|
76
|
+
attachedCallback: "webComponentAttached",
|
|
77
|
+
connectedCallback: "webComponentConnected",
|
|
78
|
+
disconnectedCallback: "webComponentDisconnected",
|
|
79
|
+
adoptedCallback: "webComponentAdopted"
|
|
80
|
+
};
|
|
81
|
+
module2.exports = {
|
|
82
|
+
/*
|
|
83
|
+
* @param {JSX.Element} wrapper: the wrapper component class to be instantiated and wrapped
|
|
84
|
+
* @param {string} tagName - The name of the web component. Has to be minus "-" delimited.
|
|
85
|
+
* @param {boolean} useShadowDom - If the value is set to "true" the web component will use the `shadowDom`. The default value is true.
|
|
86
|
+
*/
|
|
87
|
+
create: (wrapper, tagName, useShadowDom = true, compRef = void 0) => {
|
|
88
|
+
const proto = class extends HTMLElement {
|
|
89
|
+
instance = null;
|
|
90
|
+
// the instance we create of the wrapper
|
|
91
|
+
callConstructorHook() {
|
|
92
|
+
if (this.instance["webComponentConstructed"]) {
|
|
93
|
+
this.instance["webComponentConstructed"].apply(this.instance, [this]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
callLifeCycleHook(hook, params = []) {
|
|
97
|
+
const method = lifeCycleHooks[hook];
|
|
98
|
+
if (method && this.instance && this.instance[method]) {
|
|
99
|
+
this.instance[method].apply(this.instance, params);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
connectedCallback() {
|
|
103
|
+
const self = this;
|
|
104
|
+
let mountPoint = self;
|
|
105
|
+
if (useShadowDom) {
|
|
106
|
+
const shadowRoot = self.attachShadow({ mode: "open" });
|
|
107
|
+
mountPoint = document.createElement("div");
|
|
108
|
+
const styles2 = getStyleElementsFromReactWebComponentStyleLoader();
|
|
109
|
+
styles2.forEach((style) => {
|
|
110
|
+
shadowRoot.appendChild(style.cloneNode(shadowRoot));
|
|
111
|
+
});
|
|
112
|
+
shadowRoot.appendChild(mountPoint);
|
|
113
|
+
retargetEvents(shadowRoot);
|
|
114
|
+
}
|
|
115
|
+
ReactDOM.render(
|
|
116
|
+
// This is where we instantiate the actual component (in its wrapper)
|
|
117
|
+
React3.createElement(wrapper, extractAttributes(self)),
|
|
118
|
+
mountPoint,
|
|
119
|
+
function() {
|
|
120
|
+
self.instance = this;
|
|
121
|
+
self.callConstructorHook();
|
|
122
|
+
self.callLifeCycleHook("connectedCallback");
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
disconnectedCallback() {
|
|
127
|
+
this.callLifeCycleHook("disconnectedCallback");
|
|
128
|
+
}
|
|
129
|
+
adoptedCallback(oldDocument, newDocument) {
|
|
130
|
+
this.callLifeCycleHook("adoptedCallback", [oldDocument, newDocument]);
|
|
131
|
+
}
|
|
132
|
+
/* call a function defined in the component, either as a class method, or
|
|
133
|
+
* via useImperativeHandle */
|
|
134
|
+
call(functionName, args) {
|
|
135
|
+
return compRef?.current?.[functionName]?.call(compRef?.current, args);
|
|
136
|
+
}
|
|
137
|
+
/* predefined function to retrieve the pre-defined config object of the
|
|
138
|
+
* state, populated via the pre-defined `setConfig` method given as prop
|
|
139
|
+
* to the wrapped component. */
|
|
140
|
+
getConfig() {
|
|
141
|
+
return this.instance.state.config;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
customElements.define(tagName, proto);
|
|
145
|
+
return proto;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ../common/constants.js
|
|
152
|
+
var require_constants = __commonJS({
|
|
153
|
+
"../common/constants.js"(exports, module2) {
|
|
154
|
+
module2.exports = {
|
|
155
|
+
rosReleases: {
|
|
156
|
+
kinetic: { rosVersion: 1, ubuntuCodename: "xenial" },
|
|
157
|
+
melodic: { rosVersion: 1, ubuntuCodename: "bionic" },
|
|
158
|
+
noetic: { rosVersion: 1, ubuntuCodename: "focal" },
|
|
159
|
+
dashing: { rosVersion: 2 },
|
|
160
|
+
eloquent: { rosVersion: 2 },
|
|
161
|
+
foxy: { rosVersion: 2 },
|
|
162
|
+
galactic: { rosVersion: 2 },
|
|
163
|
+
humble: { rosVersion: 2 },
|
|
164
|
+
iron: { rosVersion: 2 },
|
|
165
|
+
rolling: { rosVersion: 2 }
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ../common/common.js
|
|
172
|
+
var require_common = __commonJS({
|
|
173
|
+
"../common/common.js"(exports, module2) {
|
|
174
|
+
var semverCompare = require("semver/functions/compare");
|
|
175
|
+
var semverMinVersion = require("semver/ranges/min-version");
|
|
176
|
+
var _2 = {
|
|
177
|
+
get: require("lodash/get"),
|
|
178
|
+
set: require("lodash/set"),
|
|
179
|
+
unset: require("lodash/unset"),
|
|
180
|
+
forEach: require("lodash/forEach"),
|
|
181
|
+
map: require("lodash/map"),
|
|
182
|
+
isEmpty: require("lodash/isEmpty"),
|
|
183
|
+
eq: require("lodash/isEqual"),
|
|
184
|
+
isPlainObject: require("lodash/isPlainObject"),
|
|
185
|
+
merge: require("lodash/merge")
|
|
186
|
+
};
|
|
187
|
+
var loglevel = require("loglevel");
|
|
188
|
+
var chalk = require("chalk");
|
|
189
|
+
var constants = require_constants();
|
|
190
|
+
loglevel.setAll = (level) => Object.values(loglevel.getLoggers()).forEach((l) => l.setLevel(level));
|
|
191
|
+
var methodColors = {
|
|
192
|
+
warn: chalk.yellow,
|
|
193
|
+
error: chalk.red,
|
|
194
|
+
info: chalk.green,
|
|
195
|
+
debug: chalk.gray
|
|
196
|
+
};
|
|
197
|
+
var coloredMethod = (method) => methodColors[method] ? methodColors[method](method) : method;
|
|
198
|
+
var originalFactory = loglevel.methodFactory;
|
|
199
|
+
loglevel.methodFactory = (methodName, level, loggerName) => {
|
|
200
|
+
const rawMethod = originalFactory(methodName, level, loggerName);
|
|
201
|
+
if (typeof window != "undefined") {
|
|
202
|
+
const context2 = `${loggerName} ${methodName}`;
|
|
203
|
+
return (...args) => rawMethod(`[${context2}]`, ...args);
|
|
204
|
+
}
|
|
205
|
+
const context = `${loggerName} ${coloredMethod(methodName)}`;
|
|
206
|
+
return (...args) => rawMethod(
|
|
207
|
+
`[${chalk.blue((/* @__PURE__ */ new Date()).toISOString())} ${context}]`,
|
|
208
|
+
...args
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
var getLogger2 = loglevel.getLogger;
|
|
212
|
+
var clone2 = (obj) => JSON.parse(JSON.stringify(obj));
|
|
213
|
+
var decodeJWT3 = (jwt) => JSON.parse(atob(jwt.split(".")[1]));
|
|
214
|
+
var tryJSONParse = (string) => {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(string);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var visit = (object, childField, visitor) => {
|
|
222
|
+
if (!object)
|
|
223
|
+
return;
|
|
224
|
+
visitor(object);
|
|
225
|
+
object[childField]?.forEach((child) => visit(child, childField, visitor));
|
|
226
|
+
};
|
|
227
|
+
var visitAncestor = (object, path, visitor, prefix = []) => {
|
|
228
|
+
visitor(object, prefix);
|
|
229
|
+
const next = path[0];
|
|
230
|
+
if (next) {
|
|
231
|
+
const sub = object[next];
|
|
232
|
+
if (sub) {
|
|
233
|
+
visitAncestor(sub, path.slice(1), visitor, prefix.concat(next));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var wait = (delay) => new Promise((resolve) => {
|
|
238
|
+
setTimeout(resolve, delay);
|
|
239
|
+
});
|
|
240
|
+
var toFlatObject = (obj, prefix = [], rtv = {}) => {
|
|
241
|
+
_2.forEach(obj, (value, key) => {
|
|
242
|
+
const newPrefix = prefix.concat(String(key));
|
|
243
|
+
if ((_2.isPlainObject(value) || value instanceof Array) && value !== null) {
|
|
244
|
+
toFlatObject(value, newPrefix, rtv);
|
|
245
|
+
} else {
|
|
246
|
+
rtv[pathToTopic2(newPrefix)] = value;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return rtv;
|
|
250
|
+
};
|
|
251
|
+
var forMatchIterator = (obj, path, callback, pathSoFar = [], matchSoFar = {}) => {
|
|
252
|
+
if (path.length == 0 || path[0] == "#") {
|
|
253
|
+
callback(obj, pathSoFar, matchSoFar);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const next = path[0];
|
|
257
|
+
if (next) {
|
|
258
|
+
for (let key in obj) {
|
|
259
|
+
if (key == next || next == "*" || next.startsWith("+")) {
|
|
260
|
+
const match = next.startsWith("+") && next.length > 1 ? Object.assign({}, matchSoFar, { [next.slice(1)]: key }) : matchSoFar;
|
|
261
|
+
forMatchIterator(
|
|
262
|
+
obj[key],
|
|
263
|
+
path.slice(1),
|
|
264
|
+
callback,
|
|
265
|
+
pathSoFar.concat([key]),
|
|
266
|
+
match
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
var setFromPath = (obj, path, value) => {
|
|
273
|
+
if (path.length == 0)
|
|
274
|
+
return obj;
|
|
275
|
+
const next = path.shift();
|
|
276
|
+
if (path.length == 0) {
|
|
277
|
+
obj[next] = value;
|
|
278
|
+
} else {
|
|
279
|
+
if (!obj[next])
|
|
280
|
+
obj[next] = {};
|
|
281
|
+
setFromPath(obj[next], path, value);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
var encodeTopicElement = (x) => x.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
285
|
+
var decodeTopicElement = (x) => x.replace(/%25/g, "%").replace(/%2F/g, "/");
|
|
286
|
+
var pathToTopic2 = (pathArray) => {
|
|
287
|
+
const dropWildcardIds = (x) => x.startsWith("+") ? "+" : x;
|
|
288
|
+
return `/${pathArray.map(dropWildcardIds).map(encodeTopicElement).join("/")}`;
|
|
289
|
+
};
|
|
290
|
+
var topicToPath2 = (topic) => {
|
|
291
|
+
const path = topic.split("/").map(decodeTopicElement);
|
|
292
|
+
path.length > 0 && path[0].length == 0 && path.shift();
|
|
293
|
+
path.length > 0 && path.at(-1).length == 0 && path.pop();
|
|
294
|
+
return path;
|
|
295
|
+
};
|
|
296
|
+
var topicMatch = (selector, topic) => {
|
|
297
|
+
const byArray = (s, p) => {
|
|
298
|
+
if (s.length == 0)
|
|
299
|
+
return true;
|
|
300
|
+
if (s[0][0] == "#")
|
|
301
|
+
return true;
|
|
302
|
+
if (p.length == 0)
|
|
303
|
+
return true;
|
|
304
|
+
if (s[0] == p[0])
|
|
305
|
+
return byArray(s.slice(1), p.slice(1));
|
|
306
|
+
if (s[0][0] == "+") {
|
|
307
|
+
const sub = byArray(s.slice(1), p.slice(1));
|
|
308
|
+
return sub && Object.assign({ [s[0].slice(1)]: p[0] }, sub);
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
};
|
|
312
|
+
const selectorArray = topicToPath2(selector);
|
|
313
|
+
const pathArray = topicToPath2(topic);
|
|
314
|
+
return byArray(selectorArray, pathArray);
|
|
315
|
+
};
|
|
316
|
+
var isSubTopicOf = (sub, parent) => {
|
|
317
|
+
const pPath = topicToPath2(parent);
|
|
318
|
+
const sPath = topicToPath2(sub);
|
|
319
|
+
return isPrefixOf(pPath, sPath) && pPath.length < sPath.length;
|
|
320
|
+
};
|
|
321
|
+
var isPrefixOf = (prefixArray, array) => {
|
|
322
|
+
if (prefixArray.length == 0)
|
|
323
|
+
return true;
|
|
324
|
+
return prefixArray[0] == array[0] && isPrefixOf(prefixArray.slice(1), array.slice(1));
|
|
325
|
+
};
|
|
326
|
+
var parseMQTTUsername = (username) => {
|
|
327
|
+
const parts = username.split(":");
|
|
328
|
+
return {
|
|
329
|
+
organization: parts[0],
|
|
330
|
+
client: parts[1],
|
|
331
|
+
sub: parts.slice(2)
|
|
332
|
+
};
|
|
333
|
+
};
|
|
334
|
+
var parseMQTTTopic = (topic) => {
|
|
335
|
+
const parts = topicToPath2(topic);
|
|
336
|
+
return {
|
|
337
|
+
organization: parts[0],
|
|
338
|
+
device: parts[1],
|
|
339
|
+
capabilityScope: parts[2],
|
|
340
|
+
capabilityName: parts[3],
|
|
341
|
+
capability: `${parts[2]}/${parts[3]}`,
|
|
342
|
+
version: parts[4],
|
|
343
|
+
sub: parts.slice(5)
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
var mqttParsePayload = (payload) => payload.length == 0 ? null : JSON.parse(payload.toString("utf-8"));
|
|
347
|
+
var mqttClearRetained = (mqttClient, prefixes, callback, delay = 1e3) => {
|
|
348
|
+
const toDelete = [];
|
|
349
|
+
const collectToDelete = (topic) => {
|
|
350
|
+
prefixes.forEach(
|
|
351
|
+
(prefix) => (
|
|
352
|
+
// mqttTopicMatch(topic, `${prefix}/#`) && toDelete.push(topic)
|
|
353
|
+
topicMatch(`${prefix}/#`, topic) && toDelete.push(topic)
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
mqttClient.on("message", collectToDelete);
|
|
358
|
+
prefixes.forEach((prefix) => {
|
|
359
|
+
if (typeof prefix == "string") {
|
|
360
|
+
mqttClient.subscribe(`${prefix}/#`);
|
|
361
|
+
} else {
|
|
362
|
+
console.warn("Ignoring", prefix, "since it is not a string.");
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
const nullValue = typeof Buffer != "undefined" ? Buffer.alloc(0) : null;
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
mqttClient.removeListener("message", collectToDelete);
|
|
368
|
+
prefixes.forEach((prefix) => mqttClient.unsubscribe(`${prefix}/#`));
|
|
369
|
+
const count = toDelete.length;
|
|
370
|
+
console.log(`clearing ${count} retained messages from ${prefixes}`);
|
|
371
|
+
toDelete.forEach((topic) => {
|
|
372
|
+
mqttClient.publish(topic, nullValue, { retain: true });
|
|
373
|
+
});
|
|
374
|
+
callback && callback(count);
|
|
375
|
+
}, delay);
|
|
376
|
+
};
|
|
377
|
+
var getRandomId = () => Math.random().toString(36).slice(2);
|
|
378
|
+
var versionCompare = (a, b) => semverCompare(semverMinVersion(a), semverMinVersion(b));
|
|
379
|
+
var mergeVersions2 = (versionsObject, subTopic = void 0, options = {}) => {
|
|
380
|
+
if (!versionsObject) {
|
|
381
|
+
return subTopic ? _2.set({}, subTopic, versionsObject) : versionsObject;
|
|
382
|
+
}
|
|
383
|
+
const versions = Object.keys(versionsObject).filter((ver) => (!options.maxVersion || versionCompare(ver, options.maxVersion) <= 0) && (!options.minVersion || versionCompare(options.minVersion, ver) <= 0)).sort(versionCompare);
|
|
384
|
+
const merged = {};
|
|
385
|
+
const subPath = subTopic && topicToPath2(subTopic);
|
|
386
|
+
versions.forEach((nextVersion) => {
|
|
387
|
+
const newValue = subPath ? _2.get(versionsObject[nextVersion], subPath) : versionsObject[nextVersion];
|
|
388
|
+
_2.merge(merged, newValue);
|
|
389
|
+
});
|
|
390
|
+
return subPath ? _2.set({}, subPath, merged) : merged;
|
|
391
|
+
};
|
|
392
|
+
var units = ["B", "KB", "MB", "GB", "TB"];
|
|
393
|
+
var formatBytes = (bytes) => {
|
|
394
|
+
if (!bytes)
|
|
395
|
+
return "--";
|
|
396
|
+
let i = 0;
|
|
397
|
+
while (bytes > 1024) {
|
|
398
|
+
bytes /= 1024;
|
|
399
|
+
i++;
|
|
400
|
+
}
|
|
401
|
+
return `${bytes.toFixed(2)} ${units[i]}`;
|
|
402
|
+
};
|
|
403
|
+
var formatDuration = (seconds) => {
|
|
404
|
+
if (!seconds)
|
|
405
|
+
return "--";
|
|
406
|
+
const parts = {};
|
|
407
|
+
if (seconds > 3600) {
|
|
408
|
+
parts.h = Math.floor(seconds / 3600);
|
|
409
|
+
seconds = seconds % 3600;
|
|
410
|
+
}
|
|
411
|
+
if (seconds > 60) {
|
|
412
|
+
parts.m = Math.floor(seconds / 60);
|
|
413
|
+
seconds = seconds % 60;
|
|
414
|
+
}
|
|
415
|
+
parts.s = Math.floor(seconds);
|
|
416
|
+
let rtv = "";
|
|
417
|
+
parts.h > 0 && (rtv += `${parts.h}h `);
|
|
418
|
+
parts.m > 0 && (rtv += `${parts.m}m `);
|
|
419
|
+
!parts.h && (rtv += `${parts.s}s`);
|
|
420
|
+
return rtv.trim();
|
|
421
|
+
};
|
|
422
|
+
module2.exports = {
|
|
423
|
+
parseMQTTUsername,
|
|
424
|
+
parseMQTTTopic,
|
|
425
|
+
pathToTopic: pathToTopic2,
|
|
426
|
+
topicToPath: topicToPath2,
|
|
427
|
+
toFlatObject,
|
|
428
|
+
topicMatch,
|
|
429
|
+
mqttParsePayload,
|
|
430
|
+
getRandomId,
|
|
431
|
+
versionCompare,
|
|
432
|
+
loglevel,
|
|
433
|
+
getLogger: getLogger2,
|
|
434
|
+
mergeVersions: mergeVersions2,
|
|
435
|
+
mqttClearRetained,
|
|
436
|
+
isSubTopicOf,
|
|
437
|
+
clone: clone2,
|
|
438
|
+
setFromPath,
|
|
439
|
+
forMatchIterator,
|
|
440
|
+
encodeTopicElement,
|
|
441
|
+
decodeTopicElement,
|
|
442
|
+
constants,
|
|
443
|
+
visit,
|
|
444
|
+
wait,
|
|
445
|
+
formatBytes,
|
|
446
|
+
formatDuration,
|
|
447
|
+
tryJSONParse,
|
|
448
|
+
decodeJWT: decodeJWT3,
|
|
449
|
+
visitAncestor
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ../common/DataCache.js
|
|
455
|
+
var require_DataCache = __commonJS({
|
|
456
|
+
"../common/DataCache.js"(exports, module2) {
|
|
457
|
+
var _2 = {
|
|
458
|
+
get: require("lodash/get"),
|
|
459
|
+
set: require("lodash/set"),
|
|
460
|
+
unset: require("lodash/unset"),
|
|
461
|
+
forEach: require("lodash/forEach"),
|
|
462
|
+
map: require("lodash/map"),
|
|
463
|
+
isEmpty: require("lodash/isEmpty"),
|
|
464
|
+
eq: require("lodash/isEqual"),
|
|
465
|
+
isPlainObject: require("lodash/isPlainObject"),
|
|
466
|
+
merge: require("lodash/merge")
|
|
467
|
+
};
|
|
468
|
+
var { topicToPath: topicToPath2, pathToTopic: pathToTopic2, toFlatObject, topicMatch, forMatchIterator } = require_common();
|
|
469
|
+
var unset = (obj, path) => {
|
|
470
|
+
if (!path || path.length == 0)
|
|
471
|
+
return;
|
|
472
|
+
_2.unset(obj, path);
|
|
473
|
+
const parentPath = path.slice(0, -1);
|
|
474
|
+
const parent = parentPath.length == 0 ? obj : _2.get(obj, parentPath);
|
|
475
|
+
if (_2.isEmpty(parent)) {
|
|
476
|
+
return unset(obj, parentPath);
|
|
477
|
+
} else {
|
|
478
|
+
return path;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var updateObject = (obj, modifier) => {
|
|
482
|
+
_2.forEach(modifier, (value, topic) => {
|
|
483
|
+
const path = topicToPath2(topic);
|
|
484
|
+
if (value == null) {
|
|
485
|
+
unset(obj, path);
|
|
486
|
+
} else {
|
|
487
|
+
_2.set(obj, path, value);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
return obj;
|
|
491
|
+
};
|
|
492
|
+
var selectFromObject = (obj, path) => {
|
|
493
|
+
if (path.length == 0)
|
|
494
|
+
return;
|
|
495
|
+
const next = path[0];
|
|
496
|
+
if (next) {
|
|
497
|
+
for (let key in obj) {
|
|
498
|
+
if (key != next && next != "*" && !next.startsWith("+")) {
|
|
499
|
+
delete obj[key];
|
|
500
|
+
} else {
|
|
501
|
+
selectFromObject(obj[key], path.slice(1));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var DataCache = class {
|
|
507
|
+
#data = {};
|
|
508
|
+
#listeners = [];
|
|
509
|
+
#flatListeners = [];
|
|
510
|
+
constructor(data = {}) {
|
|
511
|
+
this.#data = data;
|
|
512
|
+
}
|
|
513
|
+
/** Update the object with the given value at the given path, remove empty;
|
|
514
|
+
return the flat changes (see toFlatObject). Add `tags` to updates to mark
|
|
515
|
+
them somehow based on the context, e.g., so that some subscriptions can choose
|
|
516
|
+
to ignore updates with a certain tag.
|
|
517
|
+
*/
|
|
518
|
+
updateFromArray(path, value, tags = {}) {
|
|
519
|
+
const current = _2.get(this.#data, path);
|
|
520
|
+
if (value == null) {
|
|
521
|
+
if (current === void 0 || current === null) {
|
|
522
|
+
return {};
|
|
523
|
+
} else {
|
|
524
|
+
unset(this.#data, path);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
if (_2.eq(current, value)) {
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
_2.set(this.#data, path, value);
|
|
531
|
+
}
|
|
532
|
+
const topic = pathToTopic2(path);
|
|
533
|
+
const obj = { [topic]: value };
|
|
534
|
+
let flatChanges;
|
|
535
|
+
if (value instanceof Object) {
|
|
536
|
+
const flatValue = toFlatObject(value);
|
|
537
|
+
flatChanges = {};
|
|
538
|
+
_2.forEach(flatValue, (atomic, flatKey) => {
|
|
539
|
+
flatChanges[`${topic}${flatKey}`] = atomic;
|
|
540
|
+
});
|
|
541
|
+
} else {
|
|
542
|
+
flatChanges = obj;
|
|
543
|
+
}
|
|
544
|
+
this.#listeners.forEach((fn) => fn(obj, tags));
|
|
545
|
+
this.#flatListeners.forEach((fn) => fn(flatChanges, tags));
|
|
546
|
+
return flatChanges;
|
|
547
|
+
}
|
|
548
|
+
/** Update the value at the given path (array or dot separated string) */
|
|
549
|
+
update(path, value, tags) {
|
|
550
|
+
if (typeof path == "string") {
|
|
551
|
+
return this.updateFromTopic(path, value, tags);
|
|
552
|
+
} else if (path instanceof Array) {
|
|
553
|
+
return this.updateFromArray(path, value, tags);
|
|
554
|
+
} else {
|
|
555
|
+
throw new Error("unrecognized path expression");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** Set value from the given topic (with or without leading or trailing slash) */
|
|
559
|
+
updateFromTopic(topic, value, tags) {
|
|
560
|
+
return this.updateFromArray(topicToPath2(topic), value, tags);
|
|
561
|
+
}
|
|
562
|
+
/** Update data from a modifier object where keys are topic names to be
|
|
563
|
+
interpreted as paths, and values are the values to set */
|
|
564
|
+
updateFromModifier(modifier, tags) {
|
|
565
|
+
return _2.map(modifier, (value, topic) => this.updateFromTopic(topic, value, tags));
|
|
566
|
+
}
|
|
567
|
+
/** Add a callback for all change events. */
|
|
568
|
+
subscribe(callback) {
|
|
569
|
+
if (callback instanceof Function) {
|
|
570
|
+
this.#listeners.push(callback);
|
|
571
|
+
} else {
|
|
572
|
+
console.warn("DataCache.subscribe expects a function as argument. Did you mean to use subscribePath?");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** Subscribe to a specific topic only. Callback receives
|
|
576
|
+
`value, key, matched, tags`. */
|
|
577
|
+
subscribePath(topic, callback) {
|
|
578
|
+
this.#listeners.push((changes, tags) => {
|
|
579
|
+
_2.forEach(changes, (value, key) => {
|
|
580
|
+
const matched = topicMatch(topic, key);
|
|
581
|
+
matched && callback(value, key, matched, tags);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/** Same as subscribePath but always get all changes in flat form */
|
|
586
|
+
subscribePathFlat(topic, callback) {
|
|
587
|
+
this.#flatListeners.push((changes, tags) => {
|
|
588
|
+
_2.forEach(changes, (value, key) => {
|
|
589
|
+
const matched = topicMatch(topic, key);
|
|
590
|
+
matched && callback(value, key, matched, tags);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/** Remove a callback previously registered using `subscribe`. */
|
|
595
|
+
unsubscribe(callback) {
|
|
596
|
+
this.#listeners = this.#listeners.filter((f) => f != callback);
|
|
597
|
+
}
|
|
598
|
+
/** Get sub-value at path, or entire object if none given */
|
|
599
|
+
get(path = []) {
|
|
600
|
+
return path.length == 0 ? this.#data : _2.get(this.#data, path);
|
|
601
|
+
}
|
|
602
|
+
/** Get sub-value specified by topic */
|
|
603
|
+
getByTopic(topic) {
|
|
604
|
+
return this.get(topicToPath2(topic));
|
|
605
|
+
}
|
|
606
|
+
/** Filter the object using path with wildcards */
|
|
607
|
+
filter(path) {
|
|
608
|
+
const rtv = JSON.parse(JSON.stringify(this.get()));
|
|
609
|
+
selectFromObject(rtv, path);
|
|
610
|
+
return rtv;
|
|
611
|
+
}
|
|
612
|
+
/** Filter the object using topic with wildcards */
|
|
613
|
+
filterByTopic(topic) {
|
|
614
|
+
return this.filter(topicToPath2(topic));
|
|
615
|
+
}
|
|
616
|
+
/** For each topic match, invoke the callback with the value, topic, and match
|
|
617
|
+
just like subscribePath, but on the current data rather than future changes. */
|
|
618
|
+
forMatch(topic, callback) {
|
|
619
|
+
const path = topicToPath2(topic);
|
|
620
|
+
this.forPathMatch(path, callback);
|
|
621
|
+
}
|
|
622
|
+
/** For each path match, invoke the callback with the value, topic, and match
|
|
623
|
+
just like subscribePath */
|
|
624
|
+
forPathMatch(path, callback) {
|
|
625
|
+
forMatchIterator(this.get(), path, callback);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
module2.exports = {
|
|
629
|
+
DataCache,
|
|
630
|
+
updateObject
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ../common/MqttSync.js
|
|
636
|
+
var require_MqttSync = __commonJS({
|
|
637
|
+
"../common/MqttSync.js"(exports, module2) {
|
|
638
|
+
"use strict";
|
|
639
|
+
var {
|
|
640
|
+
mqttParsePayload,
|
|
641
|
+
topicMatch,
|
|
642
|
+
topicToPath: topicToPath2,
|
|
643
|
+
pathToTopic: pathToTopic2,
|
|
644
|
+
toFlatObject,
|
|
645
|
+
getLogger: getLogger2,
|
|
646
|
+
mergeVersions: mergeVersions2,
|
|
647
|
+
parseMQTTTopic,
|
|
648
|
+
isSubTopicOf,
|
|
649
|
+
versionCompare,
|
|
650
|
+
encodeTopicElement,
|
|
651
|
+
visitAncestor
|
|
652
|
+
} = require_common();
|
|
653
|
+
var { DataCache } = require_DataCache();
|
|
654
|
+
var _2 = require("lodash");
|
|
655
|
+
var log2 = getLogger2("MqttSync");
|
|
656
|
+
var HEARTBEAT_TOPIC = "$SYS/broker/uptime";
|
|
657
|
+
var specialKey = "$_";
|
|
658
|
+
var noop = () => {
|
|
659
|
+
};
|
|
660
|
+
var clone2 = (payload) => {
|
|
661
|
+
if (typeof payload == "object") {
|
|
662
|
+
return JSON.parse(JSON.stringify(payload));
|
|
663
|
+
} else {
|
|
664
|
+
return payload;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
var ensureHashSuffix = (topic) => topic.endsWith("/#") ? topic : topic.endsWith("/") ? topic.concat("#") : topic.concat("/#");
|
|
668
|
+
var resolveDoubleSlashes = (path) => path.replace(/\/\//g, "/");
|
|
669
|
+
var MqttSync3 = class {
|
|
670
|
+
data = new DataCache();
|
|
671
|
+
/* Directory of paths we've subscribed to in this class; this matters
|
|
672
|
+
because the same mqtt client may have subscriptions to paths that we don't
|
|
673
|
+
care to store (sync). */
|
|
674
|
+
subscribedPaths = {};
|
|
675
|
+
publishedPaths = {};
|
|
676
|
+
// not used in atomic mode
|
|
677
|
+
/* Store messages retained on mqtt so we can publish what is necessary to
|
|
678
|
+
achieve the "should-be" state. Note that we cannot use a structured document
|
|
679
|
+
for storing these publishedMessages since we need to be able to store separate
|
|
680
|
+
values at non-leaf nodes in the object (just like mqtt, where you can have
|
|
681
|
+
/a/b = 1 and /a/b/c = 1 at the same time). Note: not used in atomic mode.
|
|
682
|
+
Note: we use specialKey in this DataCache to allow overlapping
|
|
683
|
+
topics (e.g., `/a/b/$_ = 1` and `/a/$_ = {b: 2}`)
|
|
684
|
+
*/
|
|
685
|
+
publishedMessages = new DataCache();
|
|
686
|
+
/* The order in which we send retained messages matters, which is why we use
|
|
687
|
+
a queue for sending things. Note that we here use the property of Map that it
|
|
688
|
+
remembers insertion order of keys. */
|
|
689
|
+
publishQueue = /* @__PURE__ */ new Map();
|
|
690
|
+
/* List of callbacks waiting for next heartbeat, gets purged with each
|
|
691
|
+
heartbeat */
|
|
692
|
+
heartbeatWaitersOnce = [];
|
|
693
|
+
heartbeats = 0;
|
|
694
|
+
beforeDisconnectHooks = [];
|
|
695
|
+
constructor({
|
|
696
|
+
mqttClient,
|
|
697
|
+
onChange,
|
|
698
|
+
ignoreRetain,
|
|
699
|
+
migrate,
|
|
700
|
+
onReady,
|
|
701
|
+
sliceTopic,
|
|
702
|
+
onHeartbeatGranted
|
|
703
|
+
}) {
|
|
704
|
+
this.mqtt = mqttClient;
|
|
705
|
+
this.mqtt.on("message", (topic, payload, packet) => {
|
|
706
|
+
const payloadString = payload && payload.toString();
|
|
707
|
+
log2.debug(
|
|
708
|
+
"got message",
|
|
709
|
+
topic,
|
|
710
|
+
payloadString.slice(0, 180),
|
|
711
|
+
payloadString.length > 180 ? `... (${payloadString.length} bytes)` : "",
|
|
712
|
+
packet.retain
|
|
713
|
+
);
|
|
714
|
+
if (topic == HEARTBEAT_TOPIC) {
|
|
715
|
+
if (this.heartbeats > 0) {
|
|
716
|
+
this.heartbeatWaitersOnce.forEach((cb) => cb());
|
|
717
|
+
this.heartbeatWaitersOnce = [];
|
|
718
|
+
}
|
|
719
|
+
if (this.heartbeats == 1 && !migrate && onReady)
|
|
720
|
+
onReady();
|
|
721
|
+
this.heartbeats++;
|
|
722
|
+
} else if (packet.retain || ignoreRetain) {
|
|
723
|
+
let path = topicToPath2(topic);
|
|
724
|
+
log2.debug("processing message", topic, path);
|
|
725
|
+
if (sliceTopic) {
|
|
726
|
+
path = path.slice(sliceTopic);
|
|
727
|
+
topic = pathToTopic2(path);
|
|
728
|
+
}
|
|
729
|
+
const json = mqttParsePayload(payload);
|
|
730
|
+
if (this.isPublished(topic)) {
|
|
731
|
+
this.publishedMessages.updateFromArray([...path, specialKey], json);
|
|
732
|
+
this.data.update(topic, json, { external: true });
|
|
733
|
+
} else if (this.isSubscribed(topic)) {
|
|
734
|
+
log2.debug("applying received update", topic);
|
|
735
|
+
const changes = this.data.update(topic, json);
|
|
736
|
+
onChange && Object.keys(changes).length > 0 && onChange(changes);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
this.mqtt.subscribe(HEARTBEAT_TOPIC, { rap: true }, (err, granted) => {
|
|
741
|
+
log2.debug(HEARTBEAT_TOPIC, { granted });
|
|
742
|
+
granted && granted.length > 0 && onHeartbeatGranted?.();
|
|
743
|
+
});
|
|
744
|
+
migrate?.length > 0 && this.migrate(migrate, () => {
|
|
745
|
+
log2.debug("done migrating", onReady);
|
|
746
|
+
onReady && this.waitForHeartbeatOnce(onReady);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Publish all values at the given level of the given object under the given
|
|
751
|
+
* topic (plus sub-key, of course).
|
|
752
|
+
* TODO: Is this OK, or do we need to go through this.publish?
|
|
753
|
+
*/
|
|
754
|
+
publishAtLevel(topic, value, level) {
|
|
755
|
+
log2.debug(`publishingAtLevel ${level}`, topic, value);
|
|
756
|
+
if (level > 0) {
|
|
757
|
+
_2.forEach(value, (subValue, subKey) => {
|
|
758
|
+
const subTopic = `${topic}/${encodeTopicElement(subKey)}`;
|
|
759
|
+
log2.debug(`publishing ${subTopic}`);
|
|
760
|
+
this.publishAtLevel(subTopic, subValue, level - 1);
|
|
761
|
+
});
|
|
762
|
+
} else {
|
|
763
|
+
this.mqtt.publish(topic, JSON.stringify(value), { retain: true }, (err) => {
|
|
764
|
+
err && log2.warn("Error when publishing migration result", err);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/** Migrate a list of `{topic, newVersion, transform}`. The version number in
|
|
769
|
+
topic will be ignored, and all versions' values will be merged, applied in
|
|
770
|
+
order, such that the latest version is applied last. */
|
|
771
|
+
migrate(list, onReady = void 0) {
|
|
772
|
+
let toGo = list.length;
|
|
773
|
+
if (toGo == 0) {
|
|
774
|
+
onReady && onReady();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const oneDown = () => --toGo == 0 && onReady && onReady();
|
|
778
|
+
list.forEach(({
|
|
779
|
+
topic,
|
|
780
|
+
newVersion,
|
|
781
|
+
transform = void 0,
|
|
782
|
+
flat = false,
|
|
783
|
+
level = 0
|
|
784
|
+
}) => {
|
|
785
|
+
log2.debug("migrating", topic, newVersion);
|
|
786
|
+
const { organization, device, capability, sub } = parseMQTTTopic(topic);
|
|
787
|
+
const prefix = `/${organization}/${device}/${capability}`;
|
|
788
|
+
const suffix = sub.length == 0 ? "/#" : pathToTopic2(sub);
|
|
789
|
+
const subTopic = `${prefix}/+${suffix}`;
|
|
790
|
+
this.subscribe(subTopic, (err) => {
|
|
791
|
+
if (err) {
|
|
792
|
+
log2.warn("Error during migration", err);
|
|
793
|
+
oneDown();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
this.waitForHeartbeatOnce(() => {
|
|
797
|
+
log2.debug("got heartbeat", topic, subTopic);
|
|
798
|
+
const all = this.data.getByTopic(prefix);
|
|
799
|
+
if (!all) {
|
|
800
|
+
this.unsubscribe(subTopic);
|
|
801
|
+
oneDown();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const merged = mergeVersions2(all, suffix, { maxVersion: newVersion });
|
|
805
|
+
const suffixMergedValue = _2.get(merged, topicToPath2(suffix));
|
|
806
|
+
const transformed = transform ? transform(suffixMergedValue) : suffixMergedValue;
|
|
807
|
+
const newTopic = resolveDoubleSlashes(`${prefix}/${newVersion}/${suffix}`);
|
|
808
|
+
log2.debug("publishing merged", newTopic);
|
|
809
|
+
if (flat) {
|
|
810
|
+
const flatObj = toFlatObject(transformed);
|
|
811
|
+
const newPath = topicToPath2(newTopic);
|
|
812
|
+
_2.forEach(flatObj, (value, key) => {
|
|
813
|
+
const keyTopic = pathToTopic2(newPath.concat(topicToPath2(key)));
|
|
814
|
+
this.mqtt.publish(
|
|
815
|
+
keyTopic,
|
|
816
|
+
JSON.stringify(value),
|
|
817
|
+
{ retain: true },
|
|
818
|
+
(err2) => {
|
|
819
|
+
err2 && log2.warn(
|
|
820
|
+
`Error when publishing migration result for ${key}`,
|
|
821
|
+
err2
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
});
|
|
826
|
+
} else {
|
|
827
|
+
this.publishAtLevel(newTopic, transformed, level);
|
|
828
|
+
}
|
|
829
|
+
this.unsubscribe(subTopic);
|
|
830
|
+
this.waitForHeartbeatOnce(() => {
|
|
831
|
+
const oldVersions = Object.keys(all).filter((v) => versionCompare(v, newVersion) < 0);
|
|
832
|
+
const prefixesToClear = oldVersions.map((oldV) => resolveDoubleSlashes(`${prefix}/${oldV}/${suffix}`));
|
|
833
|
+
log2.debug({ prefixesToClear });
|
|
834
|
+
this.clear(prefixesToClear);
|
|
835
|
+
oneDown();
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/** Delete all retained messages in a certain topic prefix, waiting for
|
|
842
|
+
a mqtt broker heartbeat to collect existing retained. Use with care, never
|
|
843
|
+
delete topics not owned by us. Harmless within capabilities, which are
|
|
844
|
+
namespaced already.
|
|
845
|
+
|
|
846
|
+
`options.filter(topic)`: a function that can be provided to further,
|
|
847
|
+
programmatically filter the set of topics to clear, e.g., to onlt clear
|
|
848
|
+
topics of old versions.
|
|
849
|
+
|
|
850
|
+
Note: This may not yet work in robot-capabilities, since the subscription
|
|
851
|
+
prefix and received topic prefix don't match (the device prefix is added to
|
|
852
|
+
subscription by localMQTT.
|
|
853
|
+
*/
|
|
854
|
+
clear(prefixes, callback = void 0, options = {}) {
|
|
855
|
+
const toDelete = [];
|
|
856
|
+
const collectToDelete = (topic) => {
|
|
857
|
+
prefixes.forEach(
|
|
858
|
+
(prefix) => topicMatch(`${prefix}/#`, topic) && (!options.filter || options.filter(topic)) && toDelete.push(topic)
|
|
859
|
+
);
|
|
860
|
+
};
|
|
861
|
+
this.mqtt.on("message", collectToDelete);
|
|
862
|
+
prefixes.forEach((prefix) => {
|
|
863
|
+
if (typeof prefix == "string") {
|
|
864
|
+
this.mqtt.subscribe(`${prefix}/#`);
|
|
865
|
+
} else {
|
|
866
|
+
log2.warn("Ignoring", prefix, "since it is not a string.");
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
const nullValue = typeof Buffer != "undefined" ? Buffer.alloc(0) : null;
|
|
870
|
+
this.waitForHeartbeatOnce(() => {
|
|
871
|
+
this.mqtt.removeListener("message", collectToDelete);
|
|
872
|
+
prefixes.forEach((prefix) => this.mqtt.unsubscribe(prefix));
|
|
873
|
+
const count = toDelete.length;
|
|
874
|
+
log2.debug(`clearing ${count} retained messages from ${prefixes}`);
|
|
875
|
+
toDelete.forEach((topic) => {
|
|
876
|
+
this.mqtt.publish(topic, nullValue, { retain: true });
|
|
877
|
+
});
|
|
878
|
+
callback && callback(count);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/** register a callback for the next heartbeat from the broker */
|
|
882
|
+
waitForHeartbeatOnce(callback) {
|
|
883
|
+
setTimeout(() => this.heartbeatWaitersOnce.push(callback), 1);
|
|
884
|
+
}
|
|
885
|
+
/** check whether we are subscribed to the given topic */
|
|
886
|
+
isSubscribed(topic) {
|
|
887
|
+
return Object.keys(this.subscribedPaths).some((subscribedTopic) => topicMatch(subscribedTopic, topic));
|
|
888
|
+
}
|
|
889
|
+
/** Check whether we are publishing the given topic in a non-atomic way.
|
|
890
|
+
This is used to determine whether to store the published value or not. */
|
|
891
|
+
isPublished(topic) {
|
|
892
|
+
return Object.keys(this.publishedPaths).some(
|
|
893
|
+
(subscribedTopic) => topicMatch(subscribedTopic, topic) && !this.publishedPaths[subscribedTopic].atomic
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
/** Subscribe to the given topic (and all sub-topics). The callback will
|
|
897
|
+
indicate success/failure, *not* a message on the topic. */
|
|
898
|
+
subscribe(topic, callback = noop) {
|
|
899
|
+
topic = ensureHashSuffix(topic);
|
|
900
|
+
log2.debug("subscribing to", topic);
|
|
901
|
+
if (this.subscribedPaths[topic]) {
|
|
902
|
+
log2.debug("already subscribed to", topic);
|
|
903
|
+
callback();
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
this.mqtt.subscribe(topic, { rap: true }, (err, granted) => {
|
|
907
|
+
log2.debug("subscribe", topic, "granted:", granted);
|
|
908
|
+
if (granted && granted.some((grant) => grant.topic == topic && grant.qos < 128)) {
|
|
909
|
+
this.subscribedPaths[topic] = 1;
|
|
910
|
+
callback(null);
|
|
911
|
+
} else {
|
|
912
|
+
callback(`not permitted to subscribe to topic ${topic}, ${JSON.stringify(granted)}`);
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
unsubscribe(topic) {
|
|
917
|
+
topic = ensureHashSuffix(topic);
|
|
918
|
+
if (this.subscribedPaths[topic]) {
|
|
919
|
+
this.mqtt.unsubscribe(topic);
|
|
920
|
+
delete this.subscribedPaths[topic];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/** Publish retained to MQTT, store as published, and return a promise */
|
|
924
|
+
_actuallyPublish(topic, value) {
|
|
925
|
+
if (!this.mqtt.connected) {
|
|
926
|
+
log2.warn("not connected, not publishing", topic);
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
log2.debug("actually publishing", topic);
|
|
930
|
+
this.mqtt.publish(
|
|
931
|
+
topic,
|
|
932
|
+
value == null ? null : JSON.stringify(value),
|
|
933
|
+
// aka "unparse payload"
|
|
934
|
+
{ retain: true }
|
|
935
|
+
);
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
/** Send all items in the queue in sequence, if any and if not already
|
|
939
|
+
running. */
|
|
940
|
+
// async _processQueue() {
|
|
941
|
+
// if (this._processing) return; // already running (and probably waiting)
|
|
942
|
+
//
|
|
943
|
+
// this._processing = true;
|
|
944
|
+
// while (this.publishQueue.length > 0) {
|
|
945
|
+
// const {topic, value} = this.publishQueue.shift();
|
|
946
|
+
// await this._actuallyPublish(topic, value);
|
|
947
|
+
// }
|
|
948
|
+
// this._processing = false;
|
|
949
|
+
// }
|
|
950
|
+
// when using Map
|
|
951
|
+
_processQueue_rec(cb) {
|
|
952
|
+
if (this.publishQueue.size > 0) {
|
|
953
|
+
const [topic, value] = this.publishQueue.entries().next().value;
|
|
954
|
+
if (this._actuallyPublish(topic, value)) {
|
|
955
|
+
this.publishQueue.delete(topic);
|
|
956
|
+
this._processQueue_rec(cb);
|
|
957
|
+
} else {
|
|
958
|
+
setTimeout(() => this._processQueue_rec(cb), 5e3);
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
cb();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
_processQueue() {
|
|
965
|
+
if (this._processing)
|
|
966
|
+
return;
|
|
967
|
+
this._processing = true;
|
|
968
|
+
this._processQueue_rec(() => this._processing = false);
|
|
969
|
+
}
|
|
970
|
+
/** Set delay between processing of publishing queue in milliseconds. This
|
|
971
|
+
allows you to effectively throttle the rate at which this instance will
|
|
972
|
+
publish changes. Note that updates to a topic already in the queue will not
|
|
973
|
+
cause multiple publications. Only the latest value will be published.
|
|
974
|
+
@param {number} [delay] - Number of milliseconds to wait between processing
|
|
975
|
+
of publish queue.
|
|
976
|
+
*/
|
|
977
|
+
setThrottle(delay) {
|
|
978
|
+
this._processQueueThrottled = _2.throttle(this._processQueue.bind(this), delay);
|
|
979
|
+
}
|
|
980
|
+
/** Clear the set throttling delay. */
|
|
981
|
+
clearThrottle() {
|
|
982
|
+
delete this._processQueueThrottled;
|
|
983
|
+
}
|
|
984
|
+
addToQueue(topic, value) {
|
|
985
|
+
this.publishQueue.set(topic, value);
|
|
986
|
+
}
|
|
987
|
+
/** Add to publication queue */
|
|
988
|
+
_enqueue(topic, value) {
|
|
989
|
+
log2.debug("enqueuing", topic);
|
|
990
|
+
this.addToQueue(topic, value);
|
|
991
|
+
if (this._processQueueThrottled) {
|
|
992
|
+
this._processQueueThrottled();
|
|
993
|
+
} else {
|
|
994
|
+
this._processQueue();
|
|
995
|
+
}
|
|
996
|
+
const path = topicToPath2(topic);
|
|
997
|
+
this.publishedMessages.updateFromArray(
|
|
998
|
+
[...path, specialKey],
|
|
999
|
+
value == null ? null : clone2(value)
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
/** Register a listener for path in data. Make sure to populate the data
|
|
1003
|
+
before calling this or set the data all at once afterwards.
|
|
1004
|
+
|
|
1005
|
+
With option "atomic" this will always send the whole sub-document,
|
|
1006
|
+
not flat changes. Useful, e.g., for desiredPackages, see
|
|
1007
|
+
https://github.com/chfritz/transitive/issues/85.
|
|
1008
|
+
|
|
1009
|
+
@return true if publication added (false, e.g., when already present)
|
|
1010
|
+
*/
|
|
1011
|
+
publish(topic, options = { atomic: false }) {
|
|
1012
|
+
topic = ensureHashSuffix(topic);
|
|
1013
|
+
if (_2.isEqual(this.publishedPaths[topic], options)) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
this.publishedPaths[topic] = options;
|
|
1017
|
+
if (options.atomic) {
|
|
1018
|
+
this.data.subscribePath(topic, (value, key, matched, tags) => {
|
|
1019
|
+
if (tags?.external)
|
|
1020
|
+
return;
|
|
1021
|
+
log2.debug("processing change (atomic)", key, topic);
|
|
1022
|
+
const topicWithoutHash = topic.slice(0, topic.length - 2);
|
|
1023
|
+
const groundedTopic = pathToTopic2(
|
|
1024
|
+
// get length of topic (how many levels of selectors), get that many
|
|
1025
|
+
// levels from key prefix
|
|
1026
|
+
topicToPath2(key).slice(0, topicToPath2(topicWithoutHash).length)
|
|
1027
|
+
);
|
|
1028
|
+
this._enqueue(groundedTopic, this.data.getByTopic(groundedTopic));
|
|
1029
|
+
});
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
this.mqtt.subscribe(topic);
|
|
1033
|
+
this.data.subscribePath(topic, (value, key, matched, tags) => {
|
|
1034
|
+
if (tags?.external)
|
|
1035
|
+
return;
|
|
1036
|
+
log2.debug("processing change", key);
|
|
1037
|
+
const path = topicToPath2(key);
|
|
1038
|
+
const publishedSub = this.publishedMessages.get(path);
|
|
1039
|
+
_2.each(publishedSub, (oldSubVal, oldSubKey) => {
|
|
1040
|
+
if (oldSubKey == specialKey)
|
|
1041
|
+
return true;
|
|
1042
|
+
const toClear = Object.keys(toFlatObject(oldSubVal)).filter((subkey) => subkey.endsWith(specialKey));
|
|
1043
|
+
log2.debug("flat->atomic: ", { toClear }, oldSubKey);
|
|
1044
|
+
toClear.forEach((oldSubSubKey) => {
|
|
1045
|
+
const oldKey = oldSubSubKey.slice(0, -(specialKey.length + 1));
|
|
1046
|
+
const clearKey = `${key}/${oldSubKey}/${oldKey}`;
|
|
1047
|
+
this._enqueue(clearKey, null);
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
const published = this.publishedMessages.get();
|
|
1051
|
+
visitAncestor(published, path.slice(0, -1), (subObj, prefix) => {
|
|
1052
|
+
const oldVal = subObj[specialKey];
|
|
1053
|
+
if (oldVal && _2.isObject(oldVal)) {
|
|
1054
|
+
log2.debug("atomic->flat", { oldVal });
|
|
1055
|
+
const prefixTopic = pathToTopic2(prefix);
|
|
1056
|
+
this._enqueue(prefixTopic, null);
|
|
1057
|
+
const flat = toFlatObject(oldVal);
|
|
1058
|
+
_2.each(flat, (flatValue, flatKey) => {
|
|
1059
|
+
const oldFlatKey = `${prefixTopic}${flatKey}`;
|
|
1060
|
+
this._enqueue(oldFlatKey, flatValue);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
this._enqueue(key, value);
|
|
1065
|
+
return true;
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
/** Run all registered hooks before disconnecting */
|
|
1069
|
+
beforeDisconnect() {
|
|
1070
|
+
this.beforeDisconnectHooks.forEach((fn) => fn(this));
|
|
1071
|
+
}
|
|
1072
|
+
/** register a new hook to be called before disconnecting */
|
|
1073
|
+
onBeforeDisconnect(fn) {
|
|
1074
|
+
this.beforeDisconnectHooks.push(fn);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
module2.exports = MqttSync3;
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// index.js
|
|
1082
|
+
var web_exports = {};
|
|
1083
|
+
__export(web_exports, {
|
|
1084
|
+
CapabilityContext: () => CapabilityContext,
|
|
1085
|
+
CapabilityContextProvider: () => CapabilityContextProvider,
|
|
1086
|
+
Code: () => Code,
|
|
1087
|
+
ErrorBoundary: () => ErrorBoundary,
|
|
1088
|
+
InlineCode: () => InlineCode,
|
|
1089
|
+
LevelBadge: () => LevelBadge,
|
|
1090
|
+
MqttSync: () => MqttSync,
|
|
1091
|
+
Timer: () => Timer,
|
|
1092
|
+
TimerContext: () => TimerContext,
|
|
1093
|
+
TransitiveCapability: () => TransitiveCapability,
|
|
1094
|
+
createWebComponent: () => createWebComponent,
|
|
1095
|
+
fetchJson: () => fetchJson,
|
|
1096
|
+
parseCookie: () => parseCookie,
|
|
1097
|
+
useCapability: () => useCapability,
|
|
1098
|
+
useMqttSync: () => useMqttSync,
|
|
1099
|
+
useTopics: () => useTopics,
|
|
1100
|
+
useTransitive: () => useTransitive
|
|
1101
|
+
});
|
|
1102
|
+
module.exports = __toCommonJS(web_exports);
|
|
1103
|
+
|
|
1104
|
+
// client/shared.jsx
|
|
1105
|
+
var import_react2 = __toESM(require("react"));
|
|
1106
|
+
var import_react_bootstrap = require("react-bootstrap");
|
|
1107
|
+
var import_react_web_component = __toESM(require_react_web_component());
|
|
1108
|
+
|
|
1109
|
+
// client/client.js
|
|
1110
|
+
var client_exports = {};
|
|
1111
|
+
__export(client_exports, {
|
|
1112
|
+
MqttSync: () => MqttSync,
|
|
1113
|
+
fetchJson: () => fetchJson,
|
|
1114
|
+
parseCookie: () => parseCookie
|
|
1115
|
+
});
|
|
1116
|
+
__reExport(client_exports, __toESM(require_common()));
|
|
1117
|
+
__reExport(client_exports, __toESM(require_DataCache()));
|
|
1118
|
+
var import_MqttSync = __toESM(require_MqttSync());
|
|
1119
|
+
var MqttSync = import_MqttSync.default;
|
|
1120
|
+
var parseCookie = (str) => str.split(";").map((v) => v.split("=")).reduce((acc, v) => {
|
|
1121
|
+
acc[decodeURIComponent(v[0].trim())] = v[1] && decodeURIComponent(v[1].trim());
|
|
1122
|
+
return acc;
|
|
1123
|
+
}, {});
|
|
1124
|
+
var fetchJson = (url, callback, options = {}) => {
|
|
1125
|
+
fetch(url, {
|
|
1126
|
+
method: options.method || (options.body ? "post" : "get"),
|
|
1127
|
+
mode: "cors",
|
|
1128
|
+
cache: "no-cache",
|
|
1129
|
+
// Maybe we'll need this (when embedding)?
|
|
1130
|
+
// credentials: 'same-origin', // include, *same-origin, omit
|
|
1131
|
+
headers: {
|
|
1132
|
+
"Content-Type": "application/json",
|
|
1133
|
+
...options.headers
|
|
1134
|
+
},
|
|
1135
|
+
redirect: "follow",
|
|
1136
|
+
referrerPolicy: "no-referrer",
|
|
1137
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
1138
|
+
}).then((res) => {
|
|
1139
|
+
const error = !res.ok && `fetching ${url} failed: ${res.status} ${res.statusText}`;
|
|
1140
|
+
res.json().then((data) => callback(error, data)).catch((err) => {
|
|
1141
|
+
throw new Error(err);
|
|
1142
|
+
});
|
|
1143
|
+
}).catch((error) => callback(`error: ${error}`));
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// client/hooks.jsx
|
|
1147
|
+
var import_react = __toESM(require("react"));
|
|
1148
|
+
var import_lodash = __toESM(require("lodash"));
|
|
1149
|
+
var import_mqtt_browser = __toESM(require("mqtt-browser"));
|
|
1150
|
+
var MqttSync2 = require_MqttSync();
|
|
1151
|
+
var log = (0, client_exports.getLogger)("utils-web/hooks");
|
|
1152
|
+
log.setLevel("info");
|
|
1153
|
+
log.setLevel("debug");
|
|
1154
|
+
var useMqttSync = ({ jwt, id, mqttUrl, appReact }) => {
|
|
1155
|
+
const { useState: useState3, useRef: useRef2, useEffect: useEffect3 } = appReact || import_react.default;
|
|
1156
|
+
const [status, setStatus] = useState3("connecting");
|
|
1157
|
+
const [mqttSync, setMqttSync] = useState3();
|
|
1158
|
+
const [data, setData] = useState3({});
|
|
1159
|
+
const [heartbeatGranted, setHeartbeatGranted] = useState3(false);
|
|
1160
|
+
useEffect3(() => {
|
|
1161
|
+
const payload = (0, client_exports.decodeJWT)(jwt);
|
|
1162
|
+
const client = import_mqtt_browser.default.connect(mqttUrl, {
|
|
1163
|
+
username: JSON.stringify({ id, payload }),
|
|
1164
|
+
password: jwt
|
|
1165
|
+
});
|
|
1166
|
+
client.on("connect", () => {
|
|
1167
|
+
log.debug("connected");
|
|
1168
|
+
const mqttSyncClient = new MqttSync2({
|
|
1169
|
+
mqttClient: client,
|
|
1170
|
+
ignoreRetain: true,
|
|
1171
|
+
onHeartbeatGranted: () => setHeartbeatGranted(true)
|
|
1172
|
+
});
|
|
1173
|
+
setMqttSync(mqttSyncClient);
|
|
1174
|
+
setStatus("connected");
|
|
1175
|
+
mqttSyncClient.data.subscribe(import_lodash.default.throttle(() => setData((0, client_exports.clone)(mqttSyncClient.data.get())), 50));
|
|
1176
|
+
});
|
|
1177
|
+
client.on("error", (error) => {
|
|
1178
|
+
log.error(error);
|
|
1179
|
+
setStatus(`error: ${error}`);
|
|
1180
|
+
});
|
|
1181
|
+
return () => {
|
|
1182
|
+
log.info("cleaning up useMQTTSync");
|
|
1183
|
+
if (mqttSync && mqttSync.beforeDisconnect) {
|
|
1184
|
+
mqttSync.beforeDisconnect();
|
|
1185
|
+
mqttSync.waitForHeartbeatOnce(() => client.end());
|
|
1186
|
+
} else {
|
|
1187
|
+
client.end();
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
}, [jwt, id]);
|
|
1191
|
+
return {
|
|
1192
|
+
status,
|
|
1193
|
+
// ready: status == 'connected',
|
|
1194
|
+
ready: heartbeatGranted,
|
|
1195
|
+
StatusComponent: () => /* @__PURE__ */ import_react.default.createElement("div", null, status),
|
|
1196
|
+
mqttSync,
|
|
1197
|
+
// Note: mqttSync.data is not reactive.
|
|
1198
|
+
data
|
|
1199
|
+
// This is a reactive data-source (to use meteor terminology).
|
|
1200
|
+
};
|
|
1201
|
+
};
|
|
1202
|
+
var useTransitive = ({ jwt, id, host, ssl, capability, versionNS, appReact }) => {
|
|
1203
|
+
const [scope, capabilityName] = capability.split("/");
|
|
1204
|
+
const { device } = (0, client_exports.decodeJWT)(jwt);
|
|
1205
|
+
const prefixPath = [id, device, scope, capabilityName];
|
|
1206
|
+
const prefix = (0, client_exports.pathToTopic)(prefixPath);
|
|
1207
|
+
const prefixPathVersion = [...prefixPath, versionNS];
|
|
1208
|
+
const prefixVersion = (0, client_exports.pathToTopic)(prefixPathVersion);
|
|
1209
|
+
const mqttUrl = `${ssl && JSON.parse(ssl) ? "wss" : "ws"}://mqtt.${host}`;
|
|
1210
|
+
const fromMqttSync = useMqttSync({ jwt, id, mqttUrl, appReact });
|
|
1211
|
+
return {
|
|
1212
|
+
...fromMqttSync,
|
|
1213
|
+
device,
|
|
1214
|
+
prefixPath,
|
|
1215
|
+
prefix,
|
|
1216
|
+
prefixPathVersion,
|
|
1217
|
+
prefixVersion
|
|
1218
|
+
};
|
|
1219
|
+
};
|
|
1220
|
+
var useTopics = ({
|
|
1221
|
+
jwt,
|
|
1222
|
+
host = "transitiverobotics.com",
|
|
1223
|
+
ssl = true,
|
|
1224
|
+
topics = [],
|
|
1225
|
+
appReact
|
|
1226
|
+
}) => {
|
|
1227
|
+
log.debug({ appReact });
|
|
1228
|
+
const { useState: useState3, useEffect: useEffect3 } = appReact || import_react.default;
|
|
1229
|
+
const [topicList, setTopicList] = useState3();
|
|
1230
|
+
!import_lodash.default.isEqual(topicList, topics) && setTopicList(topics);
|
|
1231
|
+
const { device, id, capability } = (0, client_exports.decodeJWT)(jwt);
|
|
1232
|
+
if (device == "_fleet") {
|
|
1233
|
+
log.warn("useTopics only works for device JWTs, not _fleet ones");
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const agentPrefix = `/${id}/${device}/@transitive-robotics/_robot-agent/+/status`;
|
|
1237
|
+
const { mqttSync, data, status, ready, StatusComponent } = useMqttSync({ jwt, id, mqttUrl: `ws${ssl ? "s" : ""}://mqtt.${host}`, appReact });
|
|
1238
|
+
useEffect3(() => {
|
|
1239
|
+
if (ready) {
|
|
1240
|
+
mqttSync.subscribe(agentPrefix, (err) => err && console.warn(err));
|
|
1241
|
+
}
|
|
1242
|
+
}, [mqttSync, ready]);
|
|
1243
|
+
const agentStatus = (0, client_exports.mergeVersions)(
|
|
1244
|
+
data[id]?.[device]["@transitive-robotics"]["_robot-agent"],
|
|
1245
|
+
"status"
|
|
1246
|
+
).status;
|
|
1247
|
+
const runningPackages = agentStatus?.runningPackages;
|
|
1248
|
+
const [scope, capName] = capability.split("/");
|
|
1249
|
+
const versions = runningPackages?.[scope]?.[capName];
|
|
1250
|
+
const runningVersion = versions && Object.values(versions).filter(Boolean)[0];
|
|
1251
|
+
const prefix = `/${id}/${device}/${capability}/${runningVersion}`;
|
|
1252
|
+
useEffect3(() => {
|
|
1253
|
+
log.debug("topics", topics);
|
|
1254
|
+
if (runningVersion) {
|
|
1255
|
+
topics.forEach((topic) => {
|
|
1256
|
+
log.debug(`subscribing to ${prefix}${topic}`);
|
|
1257
|
+
mqttSync.subscribe(
|
|
1258
|
+
`${prefix}${topic}`,
|
|
1259
|
+
(err) => err && log.warn(err)
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
}, [topicList, runningVersion, mqttSync]);
|
|
1264
|
+
const topicData = import_lodash.default.get(data, (0, client_exports.topicToPath)(prefix));
|
|
1265
|
+
return { data: data?.[id]?.[device], mqttSync, agentStatus, topicData };
|
|
1266
|
+
};
|
|
1267
|
+
var listeners = {};
|
|
1268
|
+
var loadedModules = {};
|
|
1269
|
+
var useCapability = ({
|
|
1270
|
+
capability,
|
|
1271
|
+
name,
|
|
1272
|
+
userId,
|
|
1273
|
+
deviceId,
|
|
1274
|
+
host = "transitiverobotics.com",
|
|
1275
|
+
ssl = true,
|
|
1276
|
+
appReact
|
|
1277
|
+
}) => {
|
|
1278
|
+
const { useState: useState3, useEffect: useEffect3 } = appReact || import_react.default;
|
|
1279
|
+
const [returns, setReturns] = useState3({ loaded: false });
|
|
1280
|
+
const done = (message, theModule) => {
|
|
1281
|
+
log.debug(`custom component ${name}: ${message}`);
|
|
1282
|
+
loadedModules[name] = theModule;
|
|
1283
|
+
setReturns((x) => ({ ...x, loadedModule: theModule, loaded: !!theModule }));
|
|
1284
|
+
};
|
|
1285
|
+
const notifyListeners = (...args) => listeners[name].forEach((l) => l(...args));
|
|
1286
|
+
useEffect3(() => {
|
|
1287
|
+
log.debug(`loading custom component ${name}`);
|
|
1288
|
+
if (loadedModules[name]) {
|
|
1289
|
+
return done("already loaded", loadedModules[name]);
|
|
1290
|
+
}
|
|
1291
|
+
if (listeners[name]) {
|
|
1292
|
+
log.debug("already loading");
|
|
1293
|
+
listeners[name].push(done);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
listeners[name] = [done];
|
|
1297
|
+
const baseUrl = `http${ssl ? "s" : ""}://portal.${host}`;
|
|
1298
|
+
const params = new URLSearchParams({ userId, deviceId });
|
|
1299
|
+
const fileBasename = `${baseUrl}/running/${capability}/dist/${name}`;
|
|
1300
|
+
import(
|
|
1301
|
+
/* webpackIgnore: true */
|
|
1302
|
+
`${fileBasename}.esm.js?${params.toString()}`
|
|
1303
|
+
).then(
|
|
1304
|
+
(esm) => notifyListeners("loaded esm", esm),
|
|
1305
|
+
(error) => {
|
|
1306
|
+
log.warn(`No ESM module found for ${name}, loading iife`, error);
|
|
1307
|
+
import(
|
|
1308
|
+
/* webpackIgnore: true */
|
|
1309
|
+
`${fileBasename}.js?${params.toString()}`
|
|
1310
|
+
).then(
|
|
1311
|
+
(iife) => notifyListeners("loaded iife", iife),
|
|
1312
|
+
(error2) => log.error(`Failed to load ${name} iife`, error2)
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
);
|
|
1316
|
+
}, [capability, name, userId, deviceId]);
|
|
1317
|
+
return returns;
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
// client/shared.jsx
|
|
1321
|
+
var styles = {
|
|
1322
|
+
badge: {
|
|
1323
|
+
width: "4em"
|
|
1324
|
+
},
|
|
1325
|
+
code: {
|
|
1326
|
+
color: "#700",
|
|
1327
|
+
borderLeft: "3px solid #aaa",
|
|
1328
|
+
padding: "0.5em 0px 0.5em 2em",
|
|
1329
|
+
backgroundColor: "#f0f0f0",
|
|
1330
|
+
borderRadius: "4px",
|
|
1331
|
+
marginTop: "0.5em"
|
|
1332
|
+
},
|
|
1333
|
+
inlineCode: {
|
|
1334
|
+
color: "#700",
|
|
1335
|
+
margin: "0px 0.5em 0px 0.5em"
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var levelBadges = [
|
|
1339
|
+
/* @__PURE__ */ import_react2.default.createElement(import_react_bootstrap.Badge, { bg: "success", style: styles.badge }, "OK"),
|
|
1340
|
+
/* @__PURE__ */ import_react2.default.createElement(import_react_bootstrap.Badge, { bg: "warning", style: styles.badge }, "Warn"),
|
|
1341
|
+
/* @__PURE__ */ import_react2.default.createElement(import_react_bootstrap.Badge, { bg: "danger", style: styles.badge }, "Error"),
|
|
1342
|
+
/* @__PURE__ */ import_react2.default.createElement(import_react_bootstrap.Badge, { bg: "secondary", style: styles.badge }, "Stale")
|
|
1343
|
+
];
|
|
1344
|
+
var LevelBadge = ({ level }) => levelBadges[level] || /* @__PURE__ */ import_react2.default.createElement("span", null, level);
|
|
1345
|
+
var Code = ({ children }) => /* @__PURE__ */ import_react2.default.createElement("pre", { style: styles.code }, children);
|
|
1346
|
+
var InlineCode = ({ children }) => /* @__PURE__ */ import_react2.default.createElement("tt", { style: styles.inlineCode }, children);
|
|
1347
|
+
var intervals = {};
|
|
1348
|
+
var TimerContext = import_react2.default.createContext({});
|
|
1349
|
+
var Timer = ({ duration, onTimeout, onStart, setOnDisconnect, children }) => {
|
|
1350
|
+
duration = duration || 60;
|
|
1351
|
+
const [timer, setTimer] = (0, import_react2.useState)(duration);
|
|
1352
|
+
const [running, setRunning] = (0, import_react2.useState)(false);
|
|
1353
|
+
const id = (0, import_react2.useMemo)(() => Math.random().toString(36).slice(2), []);
|
|
1354
|
+
const stop = () => {
|
|
1355
|
+
console.log("stopping timer for", id);
|
|
1356
|
+
onTimeout && setTimeout(onTimeout, 1);
|
|
1357
|
+
clearInterval(intervals[id]);
|
|
1358
|
+
intervals[id] = null;
|
|
1359
|
+
setRunning(false);
|
|
1360
|
+
};
|
|
1361
|
+
const startTimer = () => {
|
|
1362
|
+
const interval = intervals[id];
|
|
1363
|
+
console.log(interval, intervals, timer);
|
|
1364
|
+
if (!interval && timer > 0) {
|
|
1365
|
+
setRunning(true);
|
|
1366
|
+
intervals[id] = setInterval(() => setTimer((t) => {
|
|
1367
|
+
if (--t > 0) {
|
|
1368
|
+
return t;
|
|
1369
|
+
} else {
|
|
1370
|
+
stop();
|
|
1371
|
+
}
|
|
1372
|
+
}), 1e3);
|
|
1373
|
+
onStart && setTimeout(onStart, 1);
|
|
1374
|
+
}
|
|
1375
|
+
return stop;
|
|
1376
|
+
};
|
|
1377
|
+
(0, import_react2.useEffect)(() => {
|
|
1378
|
+
timer > 0 && !running && startTimer();
|
|
1379
|
+
}, [timer]);
|
|
1380
|
+
(0, import_react2.useEffect)(() => stop, []);
|
|
1381
|
+
setOnDisconnect && setOnDisconnect(() => {
|
|
1382
|
+
stop();
|
|
1383
|
+
});
|
|
1384
|
+
const reset = () => setTimer(duration);
|
|
1385
|
+
return /* @__PURE__ */ import_react2.default.createElement(TimerContext.Provider, { value: { reset, duration, timer } }, timer > 0 ? /* @__PURE__ */ import_react2.default.createElement("div", null, children, timer < 60 && /* @__PURE__ */ import_react2.default.createElement("div", null, "Timeout in: ", timer, " seconds")) : /* @__PURE__ */ import_react2.default.createElement("div", null, "Timed out. ", /* @__PURE__ */ import_react2.default.createElement(import_react_bootstrap.Button, { onClick: reset }, "Resume")));
|
|
1386
|
+
};
|
|
1387
|
+
var TransitiveCapability = ({
|
|
1388
|
+
jwt,
|
|
1389
|
+
host = "transitiverobotics.com",
|
|
1390
|
+
ssl = true,
|
|
1391
|
+
...config
|
|
1392
|
+
}) => {
|
|
1393
|
+
const { id, device, capability } = (0, client_exports.decodeJWT)(jwt);
|
|
1394
|
+
const type = device == "_fleet" ? "fleet" : "device";
|
|
1395
|
+
const capName = capability.split("/")[1];
|
|
1396
|
+
const name = `${capName}-${type}`;
|
|
1397
|
+
const { loaded } = useCapability({
|
|
1398
|
+
capability,
|
|
1399
|
+
name,
|
|
1400
|
+
userId: id || config.userId,
|
|
1401
|
+
// accept both id and userId, see #492
|
|
1402
|
+
deviceId: device,
|
|
1403
|
+
host,
|
|
1404
|
+
ssl
|
|
1405
|
+
});
|
|
1406
|
+
const ref = (0, import_react2.useRef)();
|
|
1407
|
+
(0, import_react2.useEffect)(() => {
|
|
1408
|
+
ref.current?.instance?.setState((s) => ({ ...s, id, jwt, host, ssl, ...config }));
|
|
1409
|
+
}, [ref.current, id, jwt, host, ssl, ...Object.values(config)]);
|
|
1410
|
+
const propClone = (0, import_react2.useMemo)(() => ({ id, jwt, host, ssl, ...config }), []);
|
|
1411
|
+
if (!loaded)
|
|
1412
|
+
return /* @__PURE__ */ import_react2.default.createElement("div", null, "Loading ", name);
|
|
1413
|
+
return import_react2.default.createElement(name, { ...propClone, ref });
|
|
1414
|
+
};
|
|
1415
|
+
var ErrorBoundary = class extends import_react2.default.Component {
|
|
1416
|
+
constructor(props) {
|
|
1417
|
+
super(props);
|
|
1418
|
+
this.state = { hasError: false };
|
|
1419
|
+
}
|
|
1420
|
+
static getDerivedStateFromError(error) {
|
|
1421
|
+
return { hasError: true };
|
|
1422
|
+
}
|
|
1423
|
+
componentDidCatch(error, errorInfo) {
|
|
1424
|
+
console.warn("ErrorBoundary caught:", error, errorInfo);
|
|
1425
|
+
}
|
|
1426
|
+
render() {
|
|
1427
|
+
return this.state.hasError ? /* @__PURE__ */ import_react2.default.createElement("div", null, this.props.message || "Something went wrong here.") : this.props.children;
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
var CapabilityContext = import_react2.default.createContext({});
|
|
1431
|
+
var LoadedCapabilityContextProvider = (props) => {
|
|
1432
|
+
const { children, jwt, id, host, ssl, loadedModule } = props;
|
|
1433
|
+
const context = loadedModule.provideContext?.({
|
|
1434
|
+
jwt,
|
|
1435
|
+
id,
|
|
1436
|
+
host,
|
|
1437
|
+
ssl,
|
|
1438
|
+
appReact: import_react2.default
|
|
1439
|
+
});
|
|
1440
|
+
return /* @__PURE__ */ import_react2.default.createElement(CapabilityContext.Provider, { value: { ...context } }, children);
|
|
1441
|
+
};
|
|
1442
|
+
var CapabilityContextProvider = ({ children, jwt, host = void 0, ssl = void 0 }) => {
|
|
1443
|
+
const { id, device, capability } = (0, client_exports.decodeJWT)(jwt);
|
|
1444
|
+
const type = device == "_fleet" ? "fleet" : "device";
|
|
1445
|
+
const capName = capability.split("/")[1];
|
|
1446
|
+
const name = `${capName}-${type}`;
|
|
1447
|
+
const { loaded, loadedModule } = useCapability({
|
|
1448
|
+
capability,
|
|
1449
|
+
name,
|
|
1450
|
+
userId: id,
|
|
1451
|
+
deviceId: device,
|
|
1452
|
+
appReact: import_react2.default,
|
|
1453
|
+
host,
|
|
1454
|
+
ssl
|
|
1455
|
+
});
|
|
1456
|
+
if (!loadedModule)
|
|
1457
|
+
return /* @__PURE__ */ import_react2.default.createElement("div", null, "Loading ", capability);
|
|
1458
|
+
return /* @__PURE__ */ import_react2.default.createElement(LoadedCapabilityContextProvider, { ...{ jwt, id, host, ssl, loadedModule } }, children);
|
|
1459
|
+
};
|
|
1460
|
+
var componentPermitsRefs = (Component) => Component.$$typeof == Symbol.for("react.forward_ref") || Component.prototype?.render;
|
|
1461
|
+
var createWebComponent = (Component, name, version = "0.0.0", options = {}) => {
|
|
1462
|
+
const compRef = componentPermitsRefs(Component) ? import_react2.default.createRef() : null;
|
|
1463
|
+
class Wrapper extends import_react2.default.Component {
|
|
1464
|
+
onDisconnect = null;
|
|
1465
|
+
state = {};
|
|
1466
|
+
/* function used by `Component` to register a onDisconnect handler */
|
|
1467
|
+
setOnDisconnect(fn) {
|
|
1468
|
+
this.onDisconnect = fn;
|
|
1469
|
+
}
|
|
1470
|
+
webComponentConstructed(instance) {
|
|
1471
|
+
const observer = new MutationObserver((mutationRecords) => {
|
|
1472
|
+
const update = {};
|
|
1473
|
+
mutationRecords.forEach(({ attributeName }) => {
|
|
1474
|
+
update[attributeName] = instance.getAttribute(attributeName);
|
|
1475
|
+
});
|
|
1476
|
+
this.setState((old) => ({ ...old, ...update }));
|
|
1477
|
+
}).observe(instance, { attributes: true });
|
|
1478
|
+
}
|
|
1479
|
+
webComponentDisconnected() {
|
|
1480
|
+
this.setState({ _disconnected: true });
|
|
1481
|
+
try {
|
|
1482
|
+
this.onDisconnect && this.onDisconnect();
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
console.log("Error during onDisconnect of web-component", e);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/* method exposed to the wrapped component via prop that allows setting
|
|
1488
|
+
* the "config" state variable inside the wrapper (not the component
|
|
1489
|
+
* itself). This config is retrieved by the portal for inclusion in the
|
|
1490
|
+
* embedding instructions. */
|
|
1491
|
+
setConfig(config) {
|
|
1492
|
+
this.setState({ config });
|
|
1493
|
+
}
|
|
1494
|
+
render() {
|
|
1495
|
+
const stylesheets = options.stylesheets || [
|
|
1496
|
+
// 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css'
|
|
1497
|
+
// Bootstrap 5.3.2 css scoped to `.transitive-bs-root`:
|
|
1498
|
+
"https://cdn.jsdelivr.net/gh/transitiverobotics/transitive-utils@0.8.3/web/css/bootstrap_transitive-bs-root.min.css"
|
|
1499
|
+
];
|
|
1500
|
+
return /* @__PURE__ */ import_react2.default.createElement(
|
|
1501
|
+
"div",
|
|
1502
|
+
{
|
|
1503
|
+
id: `cap-${name}-${version}`,
|
|
1504
|
+
className: options.className || "transitive-bs-root"
|
|
1505
|
+
},
|
|
1506
|
+
/* @__PURE__ */ import_react2.default.createElement("style", null, stylesheets.map((url) => `@import url(${url});`)),
|
|
1507
|
+
!this.state._disconnected && /* @__PURE__ */ import_react2.default.createElement(
|
|
1508
|
+
Component,
|
|
1509
|
+
{
|
|
1510
|
+
ref: compRef,
|
|
1511
|
+
...this.props,
|
|
1512
|
+
...this.state,
|
|
1513
|
+
setOnDisconnect: this.setOnDisconnect.bind(this),
|
|
1514
|
+
setConfig: this.setConfig.bind(this)
|
|
1515
|
+
}
|
|
1516
|
+
)
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
;
|
|
1521
|
+
return import_react_web_component.default.create(
|
|
1522
|
+
Wrapper,
|
|
1523
|
+
name,
|
|
1524
|
+
options.shadowDOM || false,
|
|
1525
|
+
compRef
|
|
1526
|
+
);
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
// index.js
|
|
1530
|
+
__reExport(web_exports, client_exports, module.exports);
|