UNPKG

@chromahq/store

Version:

Centralized, persistent store for Chrome extensions using zustand, accessible from service workers and React, with chrome.storage.local persistence.

423 lines (411 loc) 12.2 kB
'use strict'; var React = require('react'); var vanilla = require('zustand/vanilla'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React); function chromeStoragePersist(options) { return (config) => (set, get, store) => { const key = options.name; let isInitialized = false; let persistenceSetup = false; const initialState = config(set, get, store); const loadPersistedState = async () => { try { if (!chrome?.storage?.local) { console.warn(`Chrome storage not available for "${key}", using memory only`); isInitialized = true; setupPersistence(); return; } const result = await new Promise((resolve, reject) => { chrome.storage.local.get([key], (result2) => { if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); } else { resolve(result2); } }); }); if (result[key]) { const mergedState = { ...initialState, ...result[key] }; set(mergedState); } else { await persistState(initialState); } } catch (error) { console.error(`Failed to load persisted state for "${key}":`, error); } finally { isInitialized = true; setupPersistence(); } }; const persistState = async (state) => { if (!chrome?.storage?.local) { return; } return new Promise((resolve) => { chrome.storage.local.set({ [key]: state }, () => { if (chrome.runtime.lastError) { console.error(`Failed to persist state for "${key}":`, chrome.runtime.lastError); } resolve(); }); }); }; const setupPersistence = () => { if (persistenceSetup) return; persistenceSetup = true; store.subscribe((state) => { if (!isInitialized) return; persistState(state); }); }; loadPersistedState(); return initialState; }; } function useCentralStore(store, selector) { return React.useSyncExternalStore( store.subscribe, () => selector(store.getState()), () => selector(store.getState()) ); } function useCentralDispatch(store) { return store.setState; } class BridgeStore { constructor(bridge, initialState, storeName = "default") { this.listeners = /* @__PURE__ */ new Set(); this.currentState = null; this.previousState = null; this.ready = false; this.initialize = async () => { try { console.log("Initializing bridge store:", this.storeName, this.bridge); const state = await this.bridge.send(`store:${this.storeName}:getState`); this.previousState = this.currentState; this.currentState = state; this.notifyListeners(); this.ready = true; } catch (error) { console.warn("Failed to initialize bridge store:", error); } }; this.notifyListeners = () => { if (!this.listeners) { console.warn("BridgeStore: listeners not initialized"); return; } if (this.currentState && this.previousState) { this.listeners.forEach((listener) => listener(this.currentState, this.previousState)); } }; this.getState = () => { return this.currentState; }; this.subscribe = (listener) => { if (!this.listeners) { console.error("BridgeStore: Cannot subscribe, listeners not initialized"); return () => { }; } this.listeners.add(listener); if (this.currentState && this.previousState) { listener(this.currentState, this.previousState); } return () => { if (this.listeners) { this.listeners.delete(listener); } }; }; // Additional StoreApi methods this.destroy = () => { if (this.listeners) { this.listeners.clear(); } }; this.getInitialState = () => { return this.getState(); }; this.bridge = bridge; this.currentState = initialState || null; this.previousState = initialState || null; this.storeName = storeName; this.setupStateSync(); this.initialize(); } setupStateSync() { if (this.bridge.on) { this.bridge.on(`store:${this.storeName}:stateChanged`, (newState) => { this.previousState = this.currentState; this.currentState = newState; this.notifyListeners(); }); } } setState(partial, replace) { let actualUpdate; if (typeof partial === "function") { if (this.currentState === null) { console.warn("BridgeStore: Cannot execute function update, state not initialized"); return; } actualUpdate = partial(this.currentState); } else { actualUpdate = partial; } const payload = { partial: actualUpdate, replace }; this.bridge.send(`store:${this.storeName}:setState`, payload).catch((error) => { console.error("Failed to update state via bridge:", error); }); if (this.currentState) { this.previousState = this.currentState; if (replace) { this.currentState = actualUpdate; } else { this.currentState = { ...this.currentState, ...actualUpdate }; } this.notifyListeners(); } } } function createBridgeStore(bridge, initialState, storeName = "default") { return new BridgeStore(bridge, initialState, storeName); } function createActionHookForStore(store, actionsFactory) { return function useCustomActions() { const baseActions = useStoreActions(store); return React.useMemo(() => actionsFactory(baseActions), [baseActions]); }; } function useStoreActions(store) { return React.useMemo( () => ({ update: (partial) => { store.setState((state) => ({ ...state, ...partial })); }, updateWith: (updater) => { store.setState((state) => ({ ...state, ...updater(state) })); }, replace: (newState) => { store.setState(newState, true); }, setState: store.setState.bind(store) }), [store] ); } function createStoreHooks() { const StoreContext = React.createContext(null); function StoreProvider({ store, children }) { const storeRef = React.useRef(store); return React__namespace.createElement(StoreContext.Provider, { value: storeRef.current }, children); } function useStore(selector) { const store = React.useContext(StoreContext); if (!store) throw new Error("useStore must be used within a StoreProvider"); return useCentralStore(store, selector); } function useStoreInstance() { const store = React.useContext(StoreContext); if (!store) throw new Error("useStoreInstance must be used within a StoreProvider"); return store; } function useActions() { const store = useStoreInstance(); return useStoreActions(store); } function useAction(actionKey) { const store = useStoreInstance(); return React__namespace.useCallback( (...args) => { const fn = store.getState()[actionKey]; if (typeof fn !== "function") { throw new Error("useAction only supports function actions"); } return fn(...args); }, [store, actionKey] ); } function createActionHook(actionsFactory) { return function useCustomActions() { const store = useStoreInstance(); const baseActions = useStoreActions(store); return React.useMemo(() => actionsFactory(baseActions), [baseActions]); }; } return { createActionHook, StoreProvider, useStore, useStoreInstance, useActions, useAction }; } class StoreBuilder { constructor(name = "default") { this.config = { name, slices: [] }; } /** * Add state slices to the store */ withSlices(...slices) { this.config.slices = [...this.config.slices, ...slices]; return this; } /** * Attach a bridge for cross-context communication */ withBridge(bridge) { this.config.bridge = bridge; return this; } /** * Create the store */ async create() { if (this.config.slices.length === 0) { throw new Error("Store must have at least one slice. Use withSlices() to add state."); } return await this.createBaseStore(); } async createBaseStore() { const bridge = this.config.bridge || globalThis.bridge; if (bridge) { createBridgeStore(bridge, void 0, this.config.name); } return this.createServiceWorkerStore(); } createServiceWorkerStore() { const creator = (set, get, store2) => { let state = {}; for (const slice of this.config.slices) { const sliceState = slice(set, get, store2); state = { ...state, ...sliceState }; } return state; }; const persistOptions = { name: this.config.name }; const persistedCreator = chromeStoragePersist(persistOptions)(creator); const store = vanilla.createStore(persistedCreator); const centralStore = store; return centralStore; } } function createStore(name) { return new StoreBuilder(name); } function autoRegisterStoreHandlers(store) { if (!store) { throw new Error("autoRegisterStoreHandlers: store parameter is required"); } class AutoGetStoreStateMessage { handle() { if (!store) { throw new Error("Store instance not available"); } return store.getState(); } } class AutoSetStoreStateMessage { handle(...args) { if (!store) { throw new Error("Store instance not available"); } let partial; let replace = false; if (args.length === 1 && args[0] && typeof args[0] === "object") { const payload = args[0]; if ("partial" in payload && "replace" in payload) { ({ partial, replace } = payload); } else { partial = payload; replace = false; } } else if (args.length >= 2) { partial = args[0]; replace = args[1] || false; } else if (args.length === 1) { partial = args[0]; replace = false; } else { return store.getState(); } if (partial === void 0) { return store.getState(); } if (replace) { store.setState(partial, true); } else { store.setState(partial); } return store.getState(); } } class AutoSubscribeToStoreMessage { handle() { if (!store) { throw new Error("Store instance not available"); } store.subscribe((state, prevState) => { }); } } return { GetStoreStateMessage: AutoGetStoreStateMessage, SetStoreStateMessage: AutoSetStoreStateMessage, SubscribeToStoreMessage: AutoSubscribeToStoreMessage }; } async function init(storeDefinition) { try { let builder = createStore(storeDefinition.name); if (storeDefinition.slices) { builder = builder.withSlices(...storeDefinition.slices); } const store = await builder.create(); return { store, classes: autoRegisterStoreHandlers(store) }; } catch (error) { console.error(`Failed to initialize store "${storeDefinition.name}":`, error); throw error; } } if (typeof globalThis !== "undefined") { globalThis.__CHROMA__ = globalThis.__CHROMA__ || {}; globalThis.__CHROMA__.initStores = init; globalThis.initStores = init; } exports.BridgeStore = BridgeStore; exports.StoreBuilder = StoreBuilder; exports.chromeStoragePersist = chromeStoragePersist; exports.createActionHookForStore = createActionHookForStore; exports.createBridgeStore = createBridgeStore; exports.createStore = createStore; exports.createStoreHooks = createStoreHooks; exports.useCentralDispatch = useCentralDispatch; exports.useCentralStore = useCentralStore;