UNPKG

crann

Version:

Effortless State Synchronization for Web Extensions

4 lines (3 loc) 17.1 kB
"use strict";var le=Object.create;var k=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var fe=Object.getOwnPropertyNames;var de=Object.getPrototypeOf,ue=Object.prototype.hasOwnProperty;var Ce=(a,e)=>{for(var t in e)k(a,t,{get:e[t],enumerable:!0})},J=(a,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of fe(e))!ue.call(a,i)&&i!==t&&k(a,i,{get:()=>e[i],enumerable:!(n=X(e,i))||n.enumerable});return a};var pe=(a,e,t)=>(t=a!=null?le(de(a)):{},J(e||!a||!a.__esModule?k(t,"default",{value:a,enumerable:!0}):t,a)),Se=a=>J(k({},"__esModule",{value:!0}),a),V=(a,e,t,n)=>{for(var i=n>1?void 0:n?X(e,t):e,r=a.length-1,s;r>=0;r--)(s=a[r])&&(i=(n?s(e,t,i):s(i))||i);return n&&i&&k(e,t,i),i};var ve={};Ce(ve,{Crann:()=>F,Partition:()=>L,Persistence:()=>Q,connect:()=>H,connected:()=>oe,create:()=>ne,createConfig:()=>se,createCrannStateHook:()=>re});module.exports=Se(ve);var N=pe(require("webextension-polyfill"));var L={Instance:"instance",Service:"service"},Q={Session:"session",Local:"local",None:"none"},K=a=>!("handler"in a),q=a=>"handler"in a;var te=require("porter-source");function B(a,e){if(a===e)return!0;if(a==null||typeof a!="object"||e==null||typeof e!="object")return!1;let t=Object.keys(a),n=Object.keys(e);if(t.length!==n.length)return!1;t.sort(),n.sort();for(let i=0;i<t.length;i++){let r=t[i];if(r!==n[i]||!B(a[r],e[r]))return!1}return!0}var O=class O{static setDebug(e){O._debug=e}static isDebugEnabled(){return O._debug}};O._debug=!1;var U=O;function Z(){return U.isDebugEnabled()}function W(a,e,t){let n=t.value;return t.value=function(...i){var r;if(Z()){let s=new Error().stack,o=(r=((s==null?void 0:s.split(` `))||[])[3])==null?void 0:r.match(/at\s+(\S+)\s+/),d=o?o[1]:"unknown",g=i.length>1?i[1]:void 0,f={source:d,timestamp:Date.now(),instanceKey:i[0],changes:g};console.log("Crann State Change:",f)}return n.apply(this,i)},t}var ee=require("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",r="";return v.debug?{debug:console.log.bind(console,t,n,r,i,r),log:console.log.bind(console,t,n,r,i,r),info:console.info.bind(console,t,n,r,i,r),warn:console.warn.bind(console,t,n,r,i,r),error:console.error.bind(console,t,n,r,i,r)}:{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 b=v;function A(a,e={terse:!0}){let n=(i=>{let r=String(i);return r.length>=4?r.slice(-4):r.padStart(4,"0")})(a.location.tabId);return e!=null&&e.terse?`${n}:${a.location.frameId}`:`${a.location.context}:${n}:${a.location.frameId}`}function _(a,e,t,n,i){var g,f;let r=new Map,s=new Map,C=(g=a.context)!=null&&g.isServiceWorker?"Core":"Agent",o=b.forContext(`${C}:RPC`);return(f=a.context)!=null&&f.agentInfo&&o.setTag(A(a.context.agentInfo)),a.addEventListener("message",p=>{o.debug("Message received:",p);let[u,S]=p.data;if("call"in S&&"args"in S.call){o.debug("Processing call message:",S);let T=S.call,{id:c,args:x,target:y}=T,M=t[c];if(!M){a.postMessage([u,{error:{id:c,error:"Action not found",target:y}}]);return}try{if(M.validate&&M.validate(...x),!y){a.postMessage([u,{error:{id:c,error:"No target provided for action call",target:y}}]);return}let D=e();o.debug("Executing action with most current state:",D),Promise.resolve(M.handler(D,n,y,...x)).then(E=>{o.debug("Action handler result:",{result:E,target:y}),a.postMessage([u,{result:{id:c,result:E,target:y}}])},E=>{a.postMessage([u,{error:{id:c,error:E.message,target:y}}])})}catch(D){D instanceof Error?a.postMessage([u,{error:{id:c,error:D.message,target:y}}]):a.postMessage([u,{error:{id:c,error:"Unknown error occurred",target:y}}])}}else if("result"in S){let T=S.result,c=r.get(u);c&&(c(T.result),r.delete(u))}else if("error"in S){let T=S.error,c=r.get(u);c&&(c(Promise.reject(new Error(T.error))),r.delete(u))}else if("release"in S){let T=S.release,c=s.get(T.id);c&&(c.clear(),s.delete(T.id))}}),new Proxy({},{get(p,u){return(...S)=>{let T=Math.random();return new Promise((c,x)=>{r.set(T,y=>{y instanceof Promise?y.then(c,x):c(y)}),a.postMessage([T,{call:{id:u,args:S}}])})}}})}function z(a,e,t,n){let i=t||(0,ee.source)("crann"),r=i.type!=="agent",s=b.forContext(r?"Core:RPC":"Agent:RPC"),C={postMessage:(o,d)=>{if(r){s.debug("Posting message from service worker:",{message:o,transferables:d});let[,g]=o,f=he(g);if(!f){s.warn("No target specified for RPC response in service worker");return}i.post({action:"rpc",payload:{message:o,transferables:d||[]}},f)}else{let g=i.getAgentInfo();if(!g){s.warn("No agent info found for posting message",{agentInfo:g});return}let f=A(g),[,p]=o;"call"in p&&(p.call.target=g==null?void 0:g.location),s.withTag(f).debug("Sending RPC message from agent:",{rpcPayload:p,message:o}),i.post({action:"rpc",payload:{message:o,transferables:d||[]}})}},addEventListener:(o,d)=>{i.on({rpc:(g,f)=>{try{if(!f)s.debug("RPC message received:",{message:g,event:o});else{let c=A(f);s.withTag(c).debug("RPC message received:",{message:g,event:o})}let{payload:p}=g,{message:u,transferables:S=[]}=p,T=new MessageEvent("message",{data:u,ports:S.filter(c=>c instanceof MessagePort)||[]});d(T)}catch(p){s.error("Failed to parse RPC message payload:",p)}}})},removeEventListener:()=>{},context:{isServiceWorker:r,agentInfo:r?void 0:i.getAgentInfo()}};return _(C,a,e,n)}function he(a){if("result"in a)return a.result.target;if("error"in a)return a.error.target;if("call"in a)return a.call.target;if("release"in a)return a.release.target}var I=class I{constructor(e,t){this.config=e;this.instances=new Map;this.stateChangeListeners=[];this.instanceReadyListeners=[];this.storagePrefix="crann_";this.porter=(0,te.source)("crann",{debug:!1});t!=null&&t.debug&&(U.setDebug(!0),b.setDebug(!0)),this.storagePrefix=(t==null?void 0:t.storagePrefix)??this.storagePrefix,this.logger=b.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:(o,d)=>{if(!d){this.logger.warn("setState message heard from unknown agent");return}let g=A(d);this.logger.withTag(g).log("Setting state:",o),this.set(o.payload.state,d.id)}});let n=new Set;this.porter.onMessagesSet(o=>{if(!o){this.logger.error("Messages set but no agent info.",{info:o});return}let d=A(o);if(this.logger.withTag(d).log("onMessagesSet received for agent:",{id:o.id,context:o.location.context,tabId:o.location.tabId,frameId:o.location.frameId,alreadyInitialized:n.has(o.id)}),n.has(o.id)){this.logger.withTag(d).log("Already sent initialState to agent, skipping:",o.id);return}n.add(o.id),this.logger.withTag(d).log("Messages set received. Sending initial state.",{info:o});let g=this.get(o.id);this.porter.post({action:"initialState",payload:{state:g,info:o}},o.location),this.notifyInstanceReady(o.id,o)}),this.porter.onConnect(o=>{if(!o){this.logger.error("Agent connected but no agent info.",{info:o});return}let d=A(o);this.logger.withTag(d).log("Agent connected",{info:o}),this.addInstance(o.id,d),this.porter.onDisconnect(g=>{this.logger.withTag(A(g)).log("Agent disconnect heard. Connection type, context and location:",{info:g}),this.removeInstance(g.id)})});let r=(o,d)=>d!==void 0?this.set(o,d):this.set(o),s=this.extractActions(e),C=()=>{let o=this.get();return this.logger.log("State getter called, returning current state:",o),o};this.rpcEndpoint=z(C,s,this.porter,r)}static getInstance(e,t){return I.instance?t!=null&&t.debug&&b.forContext("Core").log("Instance requested and already existed, returning"):I.instance=new I(e,t),I.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};B(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};B(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 r=this.config[n].persist||"none",s=e?e[n]:this.serviceState[n];switch(r){case"session":await N.default.storage.session.set({[this.storagePrefix+n]:s}),t=!0;break;case"local":await N.default.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(r=>{r(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((r,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 r in e){let s=this.config[r];if(Te(s)){if(s.partition==="instance"){let C=r,o=e;n[C]=o[C]}else if(!s.partition||s.partition===L.Service){let C=r,o=e;i[C]=o[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 N.default.storage.local.get(null),t=await N.default.storage.session.get(null),n={...e,...t};this.logger.log("Storage data is:",{local:e,session:t,combined:n});let i={},r=!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 o=n[s];this.logger.log(`Found storage value for ${C}:`,o),i[C]=o,r=!0}}r?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];K(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),K(n)&&(!n.partition||n.partition===L.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=A(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])=>q(n)).reduce((t,[n,i])=>{let r=i;return{...t,[n]:r}},{})}};I.instance=null,V([W],I.prototype,"setServiceState",1),V([W],I.prototype,"setInstanceState",1);var F=I;function ne(a,e){let t=F.getInstance(a,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 Te(a){return a&&typeof a=="object"&&"default"in a}var ae=require("porter-source");var R={connected:!1},$=null;function H(a,e){let t=(e==null?void 0:e.debug)||!1,n=e==null?void 0:e.context;t&&b.setDebug(!0);let i=b.forContext("Agent"),r,s="unset",C=new Set;if(i.log("Initializing Crann Agent"+(n?` with context: ${n}`:"")),$)return i.log("We had an instance already, returning"),R.connected&&(i.log("Connect, calling onReady callback"),setTimeout(()=>{C.forEach(l=>l(R))},0)),$;i.log("No existing instance, creating a new one");let o=(0,ae.connect)({namespace:"crann",debug:!1});i.log("Porter connection created");let d=Object.entries(a).filter(([l,h])=>q(h)).reduce((l,[h,P])=>{let w=P;return{...l,[h]:{type:"action",handler:w.handler,validate:w.validate}}},{}),g=z(()=>ie(a),d,o),f=!1;o.on({initialState:l=>{if(i.log("initialState received",{alreadyReceived:f,payload:l.payload}),f){i.log("Ignoring duplicate initialState message");return}f=!0,p=l.payload.state,r=l.payload.info,s=A(r),R={connected:!0,agent:r},i.setTag(s),i.log(`Initial state received and ${S.size} listeners notified`,{message:l}),C.forEach(h=>{i.log("Calling onReady callbacks"),h(R)}),S.forEach(h=>{h.callback(p)})},stateUpdate:l=>{u=l.payload.state,p={...p,...u},i.log("State updated:",{message:l,changes:u,_state:p}),u&&S.forEach(h=>{(h.keys===void 0||h.keys.some(w=>w in u))&&h.callback(u)})}}),i.log("Porter connected. Setting up state and listeners");let p=ie(a),u=null,S=new Set;i.log("Completed setup, returning instance");let T=()=>p,c=l=>{i.log("Calling post with setState",l),o.post({action:"setState",payload:{state:l}})},x=(l,h)=>{let P={keys:h,callback:l};return S.add(P),()=>{S.delete(P)}};return $={useCrann:l=>{let h=()=>T()[l],P=j=>c({[l]:j}),w=j=>{let G=h();return x(ge=>{if(l in ge){let Y=h(),ce=T();j({current:Y,previous:G,state:ce}),G=Y}},[l])};return[h(),P,w]},get:T,set:c,subscribe:x,getAgentInfo:()=>r,onReady:l=>(i.log("onReady callback added"),C.add(l),R.connected&&(i.log("calling onReady callback"),setTimeout(()=>{l(R)},0)),()=>C.delete(l)),callAction:async(l,...h)=>(i.log("Calling action",l,h),g[l](...h))},$}function oe(){return $!==null}function ie(a){let e={};return Object.keys(a).forEach(t=>{let n=a[t];K(n)&&(e[t]=n.default)}),e}var m=require("react");function re(a){return function(t){let{useCrann:n,get:i,set:r,subscribe:s,callAction:C}=(0,m.useMemo)(()=>H(a),[t]),o=(0,m.useCallback)(f=>{let[p,u]=(0,m.useState)(i()[f]),S=(0,m.useRef)(p);(0,m.useEffect)(()=>{S.current=p},[p]),(0,m.useEffect)(()=>(u(i()[f]),s(x=>{f in x&&u(x[f])},[f])),[f]);let T=(0,m.useCallback)(c=>{r({[f]:c})},[f]);return[p,T]},[i,r,s]),d=(0,m.useCallback)(()=>i(),[i]),g=(0,m.useCallback)(f=>{r(f)},[r]);return{useStateItem:o,getState:d,setState:g,useCrann:n,callAction:C}}}function se(a){return a}0&&(module.exports={Crann,Partition,Persistence,connect,connected,create,createConfig,createCrannStateHook}); //# sourceMappingURL=index.js.map