diff --git a/packages/lexical-devtools/store.ts b/packages/lexical-devtools/store.ts index 03c2f8d6d594..21dffbcf7d3e 100644 --- a/packages/lexical-devtools/store.ts +++ b/packages/lexical-devtools/store.ts @@ -6,9 +6,10 @@ * */ -import {wrapStore} from 'webext-zustand'; import {create} from 'zustand'; +import {wrapStore} from './webext-zustand'; + interface ExtensionState { counter: number; increase: (by: number) => void; diff --git a/packages/lexical-devtools/webext-zustand.ts b/packages/lexical-devtools/webext-zustand.ts new file mode 100644 index 000000000000..a3eb830d3ab7 --- /dev/null +++ b/packages/lexical-devtools/webext-zustand.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type {StoreApi} from 'zustand'; + +import { + Store, + wrapStore as reduxWrapStore, +} from '@eduardoac-skimlinks/webext-redux'; + +/* eslint-disable no-restricted-globals */ + +export const wrapStore = async (store: StoreApi) => { + const portName = PORTNAME_PREFIX + getStoreId(store); + const serializer = (payload: unknown) => JSON.stringify(payload); + const deserializer = (payload: string) => JSON.parse(payload); + + if (isBackground()) { + handleBackground(store, { + deserializer, + portName, + serializer, + }); + + return; + } else { + return handlePages(store, { + deserializer, + portName, + serializer, + }); + } +}; + +const handleBackground = ( + store: StoreApi, + configuration?: BackgroundConfiguration, +) => { + reduxWrapStore( + { + //@ts-ignore + dispatch(data: {_sender: chrome.runtime.MessageSender; state: T}) { + store.setState(data.state); + + return {payload: data.state}; + }, + + getState: store.getState, + + subscribe: store.subscribe, + }, + configuration, + ); +}; + +const handlePages = async ( + store: StoreApi, + configuration?: PagesConfiguration, +) => { + const proxyStore = new Store(configuration); + // wait to be ready + await proxyStore.ready(); + // initial state + store.setState(proxyStore.getState()); + + const callback = (state: T, oldState: T) => { + (proxyStore.dispatch({state}) as unknown as Promise) + .then((syncedState) => { + if (syncedState) { + // success + } else { + // error (edge case) + // prevent infinite loop + unsubscribe(); + // revert + store.setState(oldState); + // resub + unsubscribe = store.subscribe(callback); + } + }) + .catch((err) => { + if ( + err instanceof Error && + err.message === 'Extension context invalidated.' + ) { + console.warn( + 'Reloading page as we lose connection to background script...', + ); + location.reload(); + return; + } + console.error('Error while during store dispatch', err); + }); + }; + + let unsubscribe = store.subscribe(callback); + + proxyStore.subscribe(() => { + // prevent retrigger + unsubscribe(); + // update + store.setState(proxyStore.getState()); + // resub + unsubscribe = store.subscribe(callback); + }); +}; + +const isBackground = () => { + const isCurrentPathname = (path?: string | null) => + path + ? new URL(path, location.origin).pathname === location.pathname + : false; + + const manifest = browser.runtime.getManifest(); + + return ( + !self.window || + (browser.extension.getBackgroundPage && + typeof window !== 'undefined' && + browser.extension.getBackgroundPage() === window) || + (manifest && + // @ts-expect-error + (isCurrentPathname(manifest.background_page) || + (manifest.background && + 'page' in manifest.background && + isCurrentPathname(manifest.background.page)) || + Boolean( + manifest.background && + 'scripts' in manifest.background && + manifest.background.scripts && + isCurrentPathname('/_generated_background_page.html'), + ))) + ); +}; + +type BackgroundConfiguration = Parameters[1]; +type PagesConfiguration = ConstructorParameters[0]; + +const getStoreId = (() => { + let id = 0; + const map = new WeakMap(); + + return (store: StoreApi): number => { + if (!map.has(store)) { + map.set(store, ++id); + } + + return map.get(store); + }; +})(); + +const PORTNAME_PREFIX = `webext-zustand-`;