@vantezzen/plasmo-state
Version:
♻️ Sync state across content script, background workers and the popup in Plasmo extensions
11 lines (8 loc) • 10.5 kB
JavaScript
var W=Object.defineProperty;var Y=(r,t,s)=>t in r?W(r,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):r[t]=s;var P=(r,t,s)=>(Y(r,typeof t!="symbol"?t+"":t,s),s),N=(r,t,s)=>{if(!t.has(r))throw TypeError("Cannot "+s)};var e=(r,t,s)=>(N(r,t,"read from private field"),s?s.call(r):t.get(r)),i=(r,t,s)=>{if(t.has(r))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(r):t.set(r,s)},c=(r,t,s,n)=>(N(r,t,"write to private field"),n?n.call(r,s):t.set(r,s),s),$=(r,t,s,n)=>({set _(o){c(r,t,o,s)},get _(){return e(r,t,n)}}),T=(r,t,s)=>(N(r,t,"access private method"),s);import tt from"debug";import et from"events";import K from"debug";import L from"webextension-polyfill";var v=(o=>(o.Popup="popup",o.Background="background",o.Content="content",o.Offscreen="offscreen",o))(v||{});var a,h,d,x,F,M=class{constructor(t){i(this,x);i(this,a,void 0);i(this,h,"plasmo-sync");i(this,d,void 0);c(this,a,t),c(this,d,K(`plasmo-state:Persistence:${e(this,a).environment}`)),this.onStateChange=this.onStateChange.bind(this),e(this,a).addListener("change",this.onStateChange),this.onBrowserStorageUpdate=this.onBrowserStorageUpdate.bind(this),e(this,a).environment!=="offscreen"&&L.storage.sync.onChanged.addListener(this.onBrowserStorageUpdate),this.fetchStateFromStorage()}onBrowserStorageUpdate(t){t[e(this,h)]&&(e(this,d).call(this,"Got storage value update info",t[e(this,h)]),T(this,x,F).call(this,t[e(this,h)].newValue))}onStateChange(t,s){if(e(this,a).environment==="offscreen"){e(this,d).call(this,"Offscreen environment: Skipping storage update onStateChange");return}if(s!=="user"||!e(this,a).keyIsPersistent(t))return;let n=e(this,a).currentRaw,o=Object.fromEntries(Object.entries(n).filter(([f])=>e(this,a).keyIsPersistent(f)));L.storage.sync.set({[e(this,h)]:JSON.stringify(o)}),e(this,d).call(this,"Pushed changed to persistent storage")}async fetchStateFromStorage(){try{if(e(this,a).environment==="offscreen")return e(this,d).call(this,"Offscreen environment: Skipping fetch from storage"),Promise.resolve();let t=await L.storage.sync.get(e(this,h));e(this,d).call(this,"fetchStateFromStorage",t),t[e(this,h)]&&T(this,x,F).call(this,t[e(this,h)])}catch(t){e(this,d).call(this,"Error in fetchStateFromStorage:",t)}finally{e(this,a).increaseReadyProgress()}}destroy(){e(this,a).removeListener("change",this.onStateChange),e(this,a).environment!=="offscreen"&&L.storage.sync.onChanged.removeListener(this.onBrowserStorageUpdate),c(this,a,null),e(this,d).call(this,"Persistence module destroyed")}};a=new WeakMap,h=new WeakMap,d=new WeakMap,x=new WeakSet,F=function(t){let s=JSON.parse(t);e(this,a).currentSource="storage";for(let[n,o]of Object.entries(s))e(this,a).current[n]=o;e(this,a).currentSource="user",e(this,d).call(this,"Fetched state from persistent storage")};import _ from"debug";import V from"debug";import w from"webextension-polyfill";var u,m=class{constructor(t){P(this,"state");i(this,u,void 0);this.state=t,c(this,u,V(`plasmo-state:State:${this.state.environment}`)),this.configUpdateListener=this.configUpdateListener.bind(this),w.runtime.onMessage.addListener(this.configUpdateListener),e(this,u).call(this,"Setup done")}configUpdateListener(t,s){var f;if(e(this,u).call(this,"Got message",t),typeof t!="object"||t.type!=="sync")return;let n=(t.tabId===-1?(f=s.tab)==null?void 0:f.id:t.tabId)??-1;if(!(this.state.tabId===n||n===-1)){e(this,u).call(this,"Ignoring message from other tab",t);return}if(t.action==="push")this.state.replace(t.data,"sync"),e(this,u).call(this,"Pushed new data from message",t);else if(t.action==="pull")return e(this,u).call(this,"Sending requested data",t),Promise.resolve(this.state.currentRaw)}async push(){if(!this.state.setupDone){e(this,u).call(this,"Tried to push before setup done");return}let t={type:"sync",action:"push",data:this.state.currentRaw,tabId:this.state.tabId};this.state.tabId===-1&&this.state.environment!=="content"&&console.warn(`[plasmo-state] You are using plasmo-state inside a non-content script context (e.g. popup or background) without providing a tab ID.
Due to this, your state changes will be pushed to *all* tabs, which may not be the intended behavior.
If you want to push state changes only to the current tab, please provide a valid tab ID in the state setup configuration, e.g.:
const plasmoState = setupState(environment, initialState, {
tabId: await browser.tabs?.query({ active: true, currentWindow: true }).then(tabs => tabs[0]?.id)
})
To find out more, visit https://github.com/vantezzen/plasmo-state#setupstate
`),this.onPush(t),w.runtime.sendMessage(t).catch(()=>{}),e(this,u).call(this,"Pushed updates")}async onPush(t){}async pull(){var s;e(this,u).call(this,"Pulling...");let t=await Promise.race([w.tabs?(s=w.tabs)==null?void 0:s.sendMessage(this.state.tabId,{type:"sync",action:"pull",tabId:this.state.tabId}).catch(()=>{}):void 0,w.runtime.sendMessage({type:"sync",action:"pull",tabId:this.state.tabId}).catch(()=>{})].filter(Boolean));if(e(this,u).call(this,"Fetched",t),t=await this.onAfterPull(t),!t){e(this,u).call(this,"Fetched, but no result");return}if(JSON.stringify(this.state.currentRaw)===JSON.stringify(t)){e(this,u).call(this,"Fetched, but no change");return}e(this,u).call(this,"Fetched, and change"),this.state.replace(t,"sync")}async onAfterPull(t){return t}destroy(){w.runtime.onMessage.removeListener(this.configUpdateListener)}};u=new WeakMap;var bt=_("plasmo-state:Sync:ContentSyncModule"),C=class extends m{};import X from"debug";import U from"webextension-polyfill";var A=X("plasmo-state:Sync:ExtensionSyncModule"),k=class extends m{constructor(t){super(t),this.state.environment==="background"&&(console.log("[plasmo-state] Adding runtime message listener for offscreen communication"),U.runtime.onMessage.addListener(this.handleRuntimeMessage.bind(this)))}handleRuntimeMessage(t,s,n){var o;if(!(t.type!=="sync"||this.state.environment!=="background")){if(console.log(`[plasmo-state] Received runtime message action=${t.action} from tabId=${t.tabId} (sender: ${(o=s.tab)==null?void 0:o.id}, url: ${s.url})`),t.action==="pushStateOffscreen"&&t.payload){console.log("[plasmo-state] Handling pushStateOffscreen",t.payload);let f=this.state.currentSource;this.state.currentSource="offscreen";try{this.state.replace(t.payload,"offscreen"),this.push()}finally{this.state.currentSource=f}}else if(t.action==="pullStateOffscreen")return console.log("[plasmo-state] Handling pullStateOffscreen, responding with currentRaw"),n(this.state.currentRaw),!0}}destroy(){this.state.environment==="background"&&(A("Background: Removing runtime message listener for offscreen communication"),U.runtime.onMessage.removeListener(this.handleRuntimeMessage.bind(this))),super.destroy()}async onPush(t){await U.tabs.sendMessage(this.state.tabId,t).catch(()=>{})}async onAfterPull(t){return t||(A("No state received from extension environment - trying content script"),await U.tabs.sendMessage(this.state.tabId,{type:"sync",action:"pull",tabId:this.state.tabId}).catch(()=>{}))}};import Z from"debug";import q from"webextension-polyfill";var S=Z("plasmo-state:Sync:OffscreenSyncModule"),O=class extends m{constructor(t){super(t),S("OffscreenSyncModule created")}async onPush(t){S("Pushing state to background:",t.payload);try{await q.runtime.sendMessage({type:"sync",action:"pushStateOffscreen",tabId:this.state.tabId,payload:t.payload})}catch(s){S("Error pushing state to background:",s)}}async pull(){S("Pulling state from background");try{let t=await Promise.race([q.runtime.sendMessage({type:"sync",action:"pullStateOffscreen",tabId:this.state.tabId}),new Promise(s=>setTimeout(()=>s(void 0),5e3))]);t?(S("Received state from background on pull:",t),this.state.replace(t,"background")):console.error("[plasmo-state] Couldn't sync with background script. Please make sure you have a background script and initialized plasmo-state in it!")}catch(t){S("Error pulling state from background:",t)}}destroy(){super.destroy(),S("OffscreenSyncModule destroyed")}};import J from"webextension-polyfill";async function z(r){if("tabs"in J){let t={active:!0};r==="popup"&&(t.currentWindow=!0);let s=await J.tabs.query(t);return!s[0]||!s[0].id?-1:s[0].id}return-1}var b,g,p,E,I,y,D,l,j,G,B,H,R=class extends et{constructor(s,n,o){super();i(this,j);i(this,B);i(this,b,void 0);i(this,g,void 0);i(this,p,void 0);i(this,E,0);P(this,"setupDone",!1);i(this,I,!1);i(this,y,void 0);i(this,D,void 0);i(this,l,void 0);P(this,"currentSource","user");c(this,g,s),c(this,l,tt(`plasmo-state:State:${s}`)),e(this,l).call(this,"State created"),c(this,b,n),c(this,p,o||{}),c(this,D,new M(this)),T(this,j,G).call(this)}increaseReadyProgress(){$(this,E)._++,e(this,E)===2&&this.emit("ready")}get current(){return new Proxy(e(this,b),{get:(s,n)=>(this.ensureNotDestroyed(),s[n]),set:(s,n,o)=>{var f;return o===s[n]||(this.ensureNotDestroyed(),s[n]=o,this.currentSource==="user"&&!this.keyIsPersistent(n)&&((f=e(this,y))==null||f.push()),this.emit("change",n,this.currentSource)),!0}})}get currentRaw(){return this.ensureNotDestroyed(),e(this,b)}get tabId(){return e(this,p).tabId}get environment(){return e(this,g)}keyIsPersistent(s){return"persistent"in e(this,p)?e(this,p).persistent.includes(s):!1}replace(s,n){this.ensureNotDestroyed(),c(this,b,s),this.emit("change","*",n)}destroy(){e(this,y).destroy(),e(this,D).destroy(),c(this,I,!0)}get isDestroyed(){return e(this,I)}ensureNotDestroyed(){if(e(this,I))throw new Error("State is destroyed")}forcePull(){return e(this,y).pull()}};b=new WeakMap,g=new WeakMap,p=new WeakMap,E=new WeakMap,I=new WeakMap,y=new WeakMap,D=new WeakMap,l=new WeakMap,j=new WeakSet,G=async function(){e(this,l).call(this,"Setting up..."),e(this,p).tabId||(e(this,p).tabId=await z(e(this,g)),e(this,l).call(this,"Dynamically got tab ID",e(this,p).tabId)),c(this,y,T(this,B,H).call(this)),e(this,l).call(this,"Sync module created, pulling initial data"),await e(this,y).pull(),this.setupDone=!0,e(this,l).call(this,"Setup done"),this.increaseReadyProgress()},B=new WeakSet,H=function(){return e(this,g)==="background"||e(this,g)==="popup"?new k(this):e(this,g)==="offscreen"?new O(this):new C(this)};import{useEffect as st,useState as rt}from"react";function Q(r){let[,t]=rt({});return st(()=>{let s=()=>{t({})};return r.addListener("change",s),()=>{r.removeListener("change",s)}},[r]),r.current}function nt(r,t,s){return new R(r,t,s)}export{R as State,v as StateEnvironment,nt as default,Q as usePlasmoState};