@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
JavaScript
'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;