UNPKG

crann

Version:

Effortless State Synchronization for Web Extensions

4 lines (3 loc) 16.3 kB
var ee=Object.defineProperty;var te=Object.getOwnPropertyDescriptor;var z=(o,e,t,n)=>{for(var i=n>1?void 0:n?te(e,t):e,a=o.length-1,s;a>=0;a--)(s=o[a])&&(i=(n?s(e,t,i):s(i))||i);return n&&i&&ee(e,t,i),i};import j from"webextension-polyfill";var U={Instance:"instance",Service:"service"},ne={Session:"session",Local:"local",None:"none"},E=o=>!("handler"in o),N=o=>"handler"in o;import{source as oe}from"porter-source";function F(o,e){if(o===e)return!0;if(o==null||typeof o!="object"||e==null||typeof e!="object")return!1;let t=Object.keys(o),n=Object.keys(e);if(t.length!==n.length)return!1;t.sort(),n.sort();for(let i=0;i<t.length;i++){let a=t[i];if(a!==n[i]||!F(o[a],e[a]))return!1}return!0}var k=class k{static setDebug(e){k._debug=e}static isDebugEnabled(){return k._debug}};k._debug=!1;var L=k;function Y(){return L.isDebugEnabled()}function H(o,e,t){let n=t.value;return t.value=function(...i){var a;if(Y()){let s=new Error().stack,l=(a=((s==null?void 0:s.split(` `))||[])[3])==null?void 0:a.match(/at\s+(\S+)\s+/),r=l?l[1]:"unknown",c=i.length>1?i[1]:void 0,g={source:r,timestamp:Date.now(),instanceKey:i[0],changes:c};console.log("Crann State Change:",g)}return n.apply(this,i)},t}import{source as ie}from"porter-source";var v=class v{constructor(e){this.tag=null;this.context=e}static setDebug(e){v.debug=e}static setPrefix(e){v.prefix=e}setTag(e){this.tag=e}withTag(e){let t=new v(this.context);return t.setTag(e),t}getFullContext(){return this.tag?`${this.context}:${this.tag}`:this.context}createLogMethods(){let e=this.getFullContext(),t=`%c${v.prefix}%c [%c${e}%c]`,n="color: #3fcbff; font-weight: bold",i="color: #d58cff; font-weight: bold",a="";return v.debug?{debug:console.log.bind(console,t,n,a,i,a),log:console.log.bind(console,t,n,a,i,a),info:console.info.bind(console,t,n,a,i,a),warn:console.warn.bind(console,t,n,a,i,a),error:console.error.bind(console,t,n,a,i,a)}:{debug:v.noOp,log:v.noOp,info:v.noOp,warn:v.noOp,error:v.noOp}}get debug(){return this.createLogMethods().debug}get log(){return this.createLogMethods().log}get info(){return this.createLogMethods().info}get warn(){return this.createLogMethods().warn}get error(){return this.createLogMethods().error}static forContext(e,t){let n=new v(e);return t&&n.setTag(t),n}};v.debug=!1,v.prefix="CrannLogger",v.noOp=function(){};var m=v;function b(o,e={terse:!0}){let n=(i=>{let a=String(i);return a.length>=4?a.slice(-4):a.padStart(4,"0")})(o.location.tabId);return e!=null&&e.terse?`${n}:${o.location.frameId}`:`${o.location.context}:${n}:${o.location.frameId}`}function X(o,e,t,n,i){var c,g;let a=new Map,s=new Map,C=(c=o.context)!=null&&c.isServiceWorker?"Core":"Agent",l=m.forContext(`${C}:RPC`);return(g=o.context)!=null&&g.agentInfo&&l.setTag(b(o.context.agentInfo)),o.addEventListener("message",p=>{l.debug("Message received:",p);let[u,S]=p.data;if("call"in S&&"args"in S.call){l.debug("Processing call message:",S);let T=S.call,{id:f,args:A,target:y}=T,R=t[f];if(!R){o.postMessage([u,{error:{id:f,error:"Action not found",target:y}}]);return}try{if(R.validate&&R.validate(...A),!y){o.postMessage([u,{error:{id:f,error:"No target provided for action call",target:y}}]);return}let P=e();l.debug("Executing action with most current state:",P),Promise.resolve(R.handler(P,n,y,...A)).then(M=>{l.debug("Action handler result:",{result:M,target:y}),o.postMessage([u,{result:{id:f,result:M,target:y}}])},M=>{o.postMessage([u,{error:{id:f,error:M.message,target:y}}])})}catch(P){P instanceof Error?o.postMessage([u,{error:{id:f,error:P.message,target:y}}]):o.postMessage([u,{error:{id:f,error:"Unknown error occurred",target:y}}])}}else if("result"in S){let T=S.result,f=a.get(u);f&&(f(T.result),a.delete(u))}else if("error"in S){let T=S.error,f=a.get(u);f&&(f(Promise.reject(new Error(T.error))),a.delete(u))}else if("release"in S){let T=S.release,f=s.get(T.id);f&&(f.clear(),s.delete(T.id))}}),new Proxy({},{get(p,u){return(...S)=>{let T=Math.random();return new Promise((f,A)=>{a.set(T,y=>{y instanceof Promise?y.then(f,A):f(y)}),o.postMessage([T,{call:{id:u,args:S}}])})}}})}function $(o,e,t,n){let i=t||ie("crann"),a=i.type!=="agent",s=m.forContext(a?"Core:RPC":"Agent:RPC"),C={postMessage:(l,r)=>{if(a){s.debug("Posting message from service worker:",{message:l,transferables:r});let[,c]=l,g=ae(c);if(!g){s.warn("No target specified for RPC response in service worker");return}i.post({action:"rpc",payload:{message:l,transferables:r||[]}},g)}else{let c=i.getAgentInfo();if(!c){s.warn("No agent info found for posting message",{agentInfo:c});return}let g=b(c),[,p]=l;"call"in p&&(p.call.target=c==null?void 0:c.location),s.withTag(g).debug("Sending RPC message from agent:",{rpcPayload:p,message:l}),i.post({action:"rpc",payload:{message:l,transferables:r||[]}})}},addEventListener:(l,r)=>{i.on({rpc:(c,g)=>{try{if(!g)s.debug("RPC message received:",{message:c,event:l});else{let f=b(g);s.withTag(f).debug("RPC message received:",{message:c,event:l})}let{payload:p}=c,{message:u,transferables:S=[]}=p,T=new MessageEvent("message",{data:u,ports:S.filter(f=>f instanceof MessagePort)||[]});r(T)}catch(p){s.error("Failed to parse RPC message payload:",p)}}})},removeEventListener:()=>{},context:{isServiceWorker:a,agentInfo:a?void 0:i.getAgentInfo()}};return X(C,o,e,n)}function ae(o){if("result"in o)return o.result.target;if("error"in o)return o.error.target;if("call"in o)return o.call.target;if("release"in o)return o.release.target}var x=class x{constructor(e,t){this.config=e;this.instances=new Map;this.stateChangeListeners=[];this.instanceReadyListeners=[];this.storagePrefix="crann_";this.porter=oe("crann",{debug:!1});var l;t!=null&&t.debug&&(L.setDebug(!0),m.setDebug(!0)),this.storagePrefix=(l=t==null?void 0:t.storagePrefix)!=null?l:this.storagePrefix,this.logger=m.forContext("Core"),this.logger.log("Constructing Crann with new logger"),this.defaultInstanceState=this.initializeInstanceDefault(),this.defaultServiceState=this.serviceState=this.initializeServiceDefault(),this.hydrate(),this.logger.log("Crann constructed, setting initial message handlers"),this.porter.on({setState:(r,c)=>{if(!c){this.logger.warn("setState message heard from unknown agent");return}let g=b(c);this.logger.withTag(g).log("Setting state:",r),this.set(r.payload.state,c.id)}});let n=new Set;this.porter.onMessagesSet(r=>{if(!r){this.logger.error("Messages set but no agent info.",{info:r});return}let c=b(r);if(this.logger.withTag(c).log("onMessagesSet received for agent:",{id:r.id,context:r.location.context,tabId:r.location.tabId,frameId:r.location.frameId,alreadyInitialized:n.has(r.id)}),n.has(r.id)){this.logger.withTag(c).log("Already sent initialState to agent, skipping:",r.id);return}n.add(r.id),this.logger.withTag(c).log("Messages set received. Sending initial state.",{info:r});let g=this.get(r.id);this.porter.post({action:"initialState",payload:{state:g,info:r}},r.location),this.notifyInstanceReady(r.id,r)}),this.porter.onConnect(r=>{if(!r){this.logger.error("Agent connected but no agent info.",{info:r});return}let c=b(r);this.logger.withTag(c).log("Agent connected",{info:r}),this.addInstance(r.id,c),this.porter.onDisconnect(g=>{this.logger.withTag(b(g)).log("Agent disconnect heard. Connection type, context and location:",{info:g}),this.removeInstance(g.id)})});let a=(r,c)=>c!==void 0?this.set(r,c):this.set(r),s=this.extractActions(e),C=()=>{let r=this.get();return this.logger.log("State getter called, returning current state:",r),r};this.rpcEndpoint=$(C,s,this.porter,a)}static getInstance(e,t){return x.instance?t!=null&&t.debug&&m.forContext("Core").log("Instance requested and already existed, returning"):x.instance=new x(e,t),x.instance}async addInstance(e,t){if(this.instances.has(e))this.logger.withTag(t).log("Instance was already registered, ignoring request from key");else{this.logger.withTag(t).log("Adding instance from agent key");let n={...this.defaultInstanceState};this.instances.set(e,n)}}async removeInstance(e){this.instances.has(e)?(this.logger.withTag(e).log("Remove instance requested"),this.instances.delete(e)):this.logger.withTag(e).log("Remove instance requested but it did not exist!")}async setServiceState(e){this.logger.log("Request to set service state with update:",e),this.logger.log("Existing service state was ",this.serviceState);let t={...this.serviceState,...e};F(this.serviceState,t)?this.logger.log("New state seems to be the same as existing, skipping"):(this.logger.log("Confirmed new state was different than existing so proceeding to persist then notify all connected instances."),this.serviceState=t,await this.persist(e),this.notify(e))}async setInstanceState(e,t){this.logger.withTag(e).log("Request to update instance state, update:",t);let n=this.instances.get(e)||this.defaultInstanceState,i={...n,...t};F(n,i)?this.logger.withTag(e).log("Instance state update is not different, skipping update."):(this.logger.withTag(e).log("Instance state update is different, updating and notifying."),this.instances.set(e,i),this.notify(t,e))}async persist(e){this.logger.log("Persisting state");let t=!1;for(let n in e||this.serviceState){let a=this.config[n].persist||"none",s=e?e[n]:this.serviceState[n];switch(a){case"session":await j.storage.session.set({[this.storagePrefix+n]:s}),t=!0;break;case"local":await j.storage.local.set({[this.storagePrefix+n]:s}),t=!0;break;default:break}}t?this.logger.log("State was persisted"):this.logger.log("Nothing to persist")}async clear(){this.logger.log("Clearing state"),this.serviceState=this.defaultServiceState,this.instances.forEach((e,t)=>{this.instances.set(t,this.defaultInstanceState)}),await this.persist(),this.notify({})}subscribe(e){this.logger.log("Subscribing to state"),this.stateChangeListeners.push(e)}notify(e,t){let n=t?this.porter.getAgentById(t):void 0,i=t?this.get(t):this.get();this.stateChangeListeners.length>0&&(this.logger.log("Notifying state change listeners in source"),this.stateChangeListeners.forEach(a=>{a(i,e,n==null?void 0:n.info)})),t&&(n!=null&&n.info.location)?(this.logger.withTag(t).log("Notifying of state change."),this.porter.post({action:"stateUpdate",payload:{state:e}},n.info.location)):(this.logger.log("Notifying everyone"),this.instances.forEach((a,s)=>{this.porter.post({action:"stateUpdate",payload:{state:e}},s)}))}get(e){return e?{...this.serviceState,...this.instances.get(e)}:{...this.serviceState}}findInstance(e){let t=this.porter.getAgentByLocation(e);if(!t)return this.logger.log("Could not find agent for location:",{location:e}),null;for(let[n,i]of this.instances)if(n===t.info.id)return this.logger.log("Found instance for key:",n),n;return this.logger.log("Could not find instance for context and location:",{location:e}),null}queryAgents(e){return this.porter.queryAgents(e)}async set(e,t){let n={},i={};for(let a in e){let s=this.config[a];if(se(s)){if(s.partition==="instance"){let C=a,l=e;n[C]=l[C]}else if(!s.partition||s.partition===U.Service){let C=a,l=e;i[C]=l[C]}}}t&&Object.keys(n).length>0&&(this.logger.withTag(t).log("Setting instance state:",n),this.setInstanceState(t,n)),Object.keys(i).length>0&&(this.logger.log("Setting service state:",i),this.setServiceState(i))}async hydrate(){this.logger.log("Hydrating state from storage.");let e=await j.storage.local.get(null),t=await j.storage.session.get(null),n={...e,...t};this.logger.log("Storage data is:",{local:e,session:t,combined:n});let i={},a=!1;for(let s in n){let C=this.removePrefix(s);if(this.logger.log(`Checking storage key ${s} -> ${C}`),this.config.hasOwnProperty(C)){let l=n[s];this.logger.log(`Found storage value for ${C}:`,l),i[C]=l,a=!0}}a?this.logger.log("Hydrated some items."):this.logger.log("No items found in storage."),this.serviceState={...this.defaultServiceState,...i}}removePrefix(e){return e.startsWith(this.storagePrefix)?e.replace(this.storagePrefix,""):e}initializeInstanceDefault(){let e={};return Object.keys(this.config).forEach(t=>{let n=this.config[t];E(n)&&n.partition==="instance"&&(e[t]=n.default)}),e}initializeServiceDefault(){this.logger.log("Initializing service default state"),this.logger.log("Config is:",this.config);let e={};return Object.keys(this.config).forEach(t=>{let n=this.config[t];this.logger.log("Item is:",n),E(n)&&(!n.partition||n.partition===U.Service)&&(e[t]=n.default,this.logger.log("Setting service state for key:",t,"to",n.default))}),this.logger.log("Final service state is:",e),e}subscribeToInstanceReady(e){return this.logger.log("Subscribing to instance ready events"),this.instanceReadyListeners.push(e),this.instances.forEach((t,n)=>{let i=this.porter.getAgentById(n);i!=null&&i.info&&e(n,i.info)}),()=>{this.logger.log("Unsubscribing from instance ready events");let t=this.instanceReadyListeners.indexOf(e);t!==-1&&this.instanceReadyListeners.splice(t,1)}}notifyInstanceReady(e,t){if(this.instanceReadyListeners.length>0){let n=b(t);this.logger.withTag(n).log("Notifying instance ready listeners"),this.instanceReadyListeners.forEach(i=>{i(e,t)})}}extractActions(e){return Object.entries(e).filter(([t,n])=>N(n)).reduce((t,[n,i])=>{let a=i;return{...t,[n]:a}},{})}};x.instance=null,z([H],x.prototype,"setServiceState",1),z([H],x.prototype,"setInstanceState",1);var q=x;function re(o,e){let t=q.getInstance(o,e);return{get:t.get.bind(t),set:t.set.bind(t),subscribe:t.subscribe.bind(t),onInstanceReady:t.subscribeToInstanceReady.bind(t),findInstance:t.findInstance.bind(t),queryAgents:t.queryAgents.bind(t),clear:t.clear.bind(t)}}function se(o){return o&&typeof o=="object"&&"default"in o}import{connect as ge}from"porter-source";var w={connected:!1},K=null;function V(o,e){let t=(e==null?void 0:e.debug)||!1,n=e==null?void 0:e.context;t&&m.setDebug(!0);let i=m.forContext("Agent"),a,s="unset",C=new Set;if(i.log("Initializing Crann Agent"+(n?` with context: ${n}`:"")),K)return i.log("We had an instance already, returning"),w.connected&&(i.log("Connect, calling onReady callback"),setTimeout(()=>{C.forEach(d=>d(w))},0)),K;i.log("No existing instance, creating a new one");let l=ge({namespace:"crann",debug:!1});i.log("Porter connection created");let r=Object.entries(o).filter(([d,h])=>N(h)).reduce((d,[h,I])=>{let D=I;return{...d,[h]:{type:"action",handler:D.handler,validate:D.validate}}},{}),c=$(()=>J(o),r,l),g=!1;l.on({initialState:d=>{if(i.log("initialState received",{alreadyReceived:g,payload:d.payload}),g){i.log("Ignoring duplicate initialState message");return}g=!0,p=d.payload.state,a=d.payload.info,s=b(a),w={connected:!0,agent:a},i.setTag(s),i.log(`Initial state received and ${S.size} listeners notified`,{message:d}),C.forEach(h=>{i.log("Calling onReady callbacks"),h(w)}),S.forEach(h=>{h.callback(p)})},stateUpdate:d=>{u=d.payload.state,p={...p,...u},i.log("State updated:",{message:d,changes:u,_state:p}),u&&S.forEach(h=>{(h.keys===void 0||h.keys.some(D=>D in u))&&h.callback(u)})}}),i.log("Porter connected. Setting up state and listeners");let p=J(o),u=null,S=new Set;i.log("Completed setup, returning instance");let T=()=>p,f=d=>{i.log("Calling post with setState",d),l.post({action:"setState",payload:{state:d}})},A=(d,h)=>{let I={keys:h,callback:d};return S.add(I),()=>{S.delete(I)}};return K={useCrann:d=>{let h=()=>T()[d],I=O=>f({[d]:O}),D=O=>{let W=h();return A(Z=>{if(d in Z){let G=h(),_=T();O({current:G,previous:W,state:_}),W=G}},[d])};return[h(),I,D]},get:T,set:f,subscribe:A,getAgentInfo:()=>a,onReady:d=>(i.log("onReady callback added"),C.add(d),w.connected&&(i.log("calling onReady callback"),setTimeout(()=>{d(w)},0)),()=>C.delete(d)),callAction:async(d,...h)=>(i.log("Calling action",d,h),c[d](...h))},K}function ce(){return K!==null}function J(o){let e={};return Object.keys(o).forEach(t=>{let n=o[t];E(n)&&(e[t]=n.default)}),e}import{useState as le,useEffect as Q,useCallback as B,useMemo as fe,useRef as de}from"react";function ue(o){return function(t){let{useCrann:n,get:i,set:a,subscribe:s,callAction:C}=fe(()=>V(o),[t]),l=B(g=>{let[p,u]=le(i()[g]),S=de(p);Q(()=>{S.current=p},[p]),Q(()=>(u(i()[g]),s(A=>{g in A&&u(A[g])},[g])),[g]);let T=B(f=>{a({[g]:f})},[g]);return[p,T]},[i,a,s]),r=B(()=>i(),[i]),c=B(g=>{a(g)},[a]);return{useStateItem:l,getState:r,setState:c,useCrann:n,callAction:C}}}function Ce(o){return o}export{q as Crann,U as Partition,ne as Persistence,V as connect,ce as connected,re as create,Ce as createConfig,ue as createCrannStateHook}; //# sourceMappingURL=index.js.map