UNPKG

pinia-react

Version:

Intuitive, type safe and flexible Store for React

395 lines (393 loc) 14.4 kB
// src/rootStore.ts var activePinia; function setActivePinia(pinia) { activePinia = pinia; } function getActivePinia() { if (!activePinia) { throw new Error( "[pinia-react] getActivePinia was called with no active Pinia. Did you forget to install pinia?\nconst pinia = createPinia() or createPinia() first.\n" ); } return activePinia; } // src/createPinia.ts function createPinia() { const state = {}; const _p = []; const _s = /* @__PURE__ */ new Map(); const _scopes = /* @__PURE__ */ new Map(); const pinia = { use(plugin) { _p.push(plugin); return this; }, _p, _s, _scopes, state }; setActivePinia(pinia); return pinia; } // src/store.ts import { enablePatches, produce, setAutoFreeze } from "immer"; import { useCallback, useRef, useSyncExternalStore } from "react"; enablePatches(); setAutoFreeze(false); var activeListenerId = null; var activeGetterKey = null; function isAffected(patches, trackedPaths) { if (trackedPaths.size === 0) return false; const tracked = Array.from(trackedPaths).map((p) => p.split(".")); for (const patch of patches) { const patchPath = patch.path.map(String); for (const trackedPath of tracked) { const len = Math.min(patchPath.length, trackedPath.length); let isPrefixMatch = true; for (let i = 0; i < len; i++) { if (patchPath[i] !== trackedPath[i]) { isPrefixMatch = false; break; } } if (isPrefixMatch) return true; } } return false; } function defineStore(id, options) { const getters = options.getters || {}; function resolveGetterDependencies(getterName, getterDepsMap, visited = /* @__PURE__ */ new Set()) { if (visited.has(getterName)) { console.warn(`[pinia-react] Circular dependency in getters detected involving: ${getterName}`); return /* @__PURE__ */ new Set(); } visited.add(getterName); const finalDeps = /* @__PURE__ */ new Set(); const directDeps = getterDepsMap.get(getterName); if (!directDeps) return finalDeps; for (const dep of directDeps) { if (dep in getters) { const nestedDeps = resolveGetterDependencies(dep, getterDepsMap, visited); nestedDeps.forEach((d) => finalDeps.add(d)); } else { finalDeps.add(dep); } } return finalDeps; } function createStoreInstance() { const pinia = getActivePinia(); const initialState = options.state(); let storePublicApi; let devTools; let isTimeTraveling = false; const localScope = { currentState: initialState, listeners: /* @__PURE__ */ new Set(), getterCache: /* @__PURE__ */ new Map(), getterDependencies: /* @__PURE__ */ new Map(), subscribers: /* @__PURE__ */ new Map(), createStoreProxy: (_onAccess) => storePublicApi }; pinia._scopes.set(id, localScope); const isGetterComputing = /* @__PURE__ */ new Set(); const emit = (nextState, oldState, patches) => { localScope.listeners.forEach((fn) => fn(nextState, oldState, patches)); localScope.subscribers.forEach((getterKeys, storeId) => { const subscriberScope = pinia._scopes.get(storeId); if (!subscriberScope) return; let shouldNotify = false; getterKeys.forEach((key) => { if (subscriberScope.getterCache.has(key)) { subscriberScope.getterCache.delete(key); shouldNotify = true; } }); if (shouldNotify) { const oldSubState = subscriberScope.currentState; const newSubState = { ...oldSubState }; subscriberScope.currentState = newSubState; pinia.state[storeId] = newSubState; subscriberScope.listeners.forEach((fn) => fn(newSubState, oldSubState, [])); } }); }; const internalPatch = (updater, actionName, isReset = false) => { if (isTimeTraveling) return; const oldState = localScope.currentState; let patches = []; const nextState = produce(oldState, updater, (p) => { patches = p; }); if (patches.length > 0 || isReset) { localScope.currentState = nextState; pinia.state[id] = nextState; if (devTools) { devTools.send({ type: actionName, payload: patches }, nextState); } emit(nextState, oldState, patches); } }; const $patch = (updater) => { internalPatch((draft) => { updater(draft); }, "@patch"); }; const $reset = () => internalPatch(() => options.state(), "@reset", true); const $subscribe = (callback) => { const listener = (state, prev, patches) => callback(state, prev); localScope.listeners.add(listener); return () => localScope.listeners.delete(listener); }; const getterInvalidationListener = (_state, _prevState, patches) => { localScope.getterDependencies.forEach((_deps, getterName) => { const resolvedDeps = resolveGetterDependencies(getterName, localScope.getterDependencies); if (isAffected(patches, resolvedDeps)) { localScope.getterCache.delete(getterName); } }); }; localScope.listeners.add(getterInvalidationListener); const originalActions = options.actions || {}; const wrappedActions = {}; const proxyTarget = {}; function createStoreProxy(onAccess) { const readonlyWarning = () => { console.warn(`[${id}] Store is read-only. Use actions for mutations.`); return false; }; const createStateProxy = (stateTarget, path, onDeepAccess) => { return new Proxy(stateTarget, { get(obj, key) { if (typeof key === "symbol") return Reflect.get(obj, key); const currentPath = [...path, String(key)]; const value = Reflect.get(obj, key); if (typeof value === "object" && value !== null) { return createStateProxy(value, currentPath, onDeepAccess); } onDeepAccess?.(currentPath); return value; }, set: readonlyWarning }); }; return new Proxy(proxyTarget, { get(_target, key, receiver) { const strKey = String(key); if (strKey === "$state") return localScope.currentState; if (strKey === "$patch") return $patch; if (strKey === "$reset") return $reset; if (strKey === "$subscribe") return $subscribe; const state = localScope.currentState; if (strKey in state) { const value = state[strKey]; if (typeof value === "object" && value !== null) { return createStateProxy(value, [strKey], onAccess); } onAccess?.([strKey]); return value; } if (strKey in getters) { onAccess?.([strKey]); if (localScope.getterCache.has(strKey)) return localScope.getterCache.get(strKey); if (isGetterComputing.has(strKey)) { console.warn(`[pinia-react] Circular dependency detected in getter "${strKey}"`); return void 0; } isGetterComputing.add(strKey); const dependencies = /* @__PURE__ */ new Set(); const prevListenerId = activeListenerId; const prevGetterKey = activeGetterKey; activeListenerId = id; activeGetterKey = strKey; try { const onGetterAccess = (path) => { dependencies.add(path[0]); }; const trackingProxyForThis = createStoreProxy(onGetterAccess); const trackingStateProxy = createStateProxy(state, [], onGetterAccess); const result = getters[strKey].call(trackingProxyForThis, trackingStateProxy); localScope.getterDependencies.set(strKey, dependencies); localScope.getterCache.set(strKey, result); return result; } finally { activeListenerId = prevListenerId; activeGetterKey = prevGetterKey; isGetterComputing.delete(strKey); } } if (strKey in wrappedActions) { return wrappedActions[strKey]; } return Reflect.get(_target, key, receiver); }, set(_target, key, value, receiver) { const strKey = String(key); if (strKey === "$state") { console.warn(`[${id}] Do not replace "$state" directly. Use "$patch()" to replace the whole state.`); return false; } if (strKey in localScope.currentState || strKey in getters || strKey in wrappedActions) { return readonlyWarning(); } return Reflect.set(_target, key, value, receiver); } }); } storePublicApi = createStoreProxy(); Object.keys(originalActions).forEach((actionName) => { const originalAction = originalActions[actionName]; wrappedActions[actionName] = (...args) => { let returnValue; const recipe = (draft) => { const actionContextProxy = new Proxy({}, { get(_, key) { const strKey = String(key); if (Reflect.has(draft, strKey)) return draft[strKey]; if (strKey in getters) { return getters[strKey].call(actionContextProxy, draft); } return Reflect.get(storePublicApi, key, storePublicApi); }, set(_, key, value) { ; draft[String(key)] = value; return true; } }); returnValue = originalAction.apply(actionContextProxy, args); }; internalPatch(recipe, actionName); return returnValue; }; }); localScope.createStoreProxy = createStoreProxy; pinia._p.forEach((plugin) => { const pluginResult = plugin({ id, store: storePublicApi, options }); if (pluginResult) { Object.defineProperties(proxyTarget, Object.getOwnPropertyDescriptors(pluginResult)); } }); pinia._s.set(id, storePublicApi); if (typeof window !== "undefined" && window.__REDUX_DEVTOOLS_EXTENSION__) { devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name: id }); devTools.init(localScope.currentState); devTools.subscribe((message) => { if (message.type === "DISPATCH") { const payloadType = message.payload?.type; switch (payloadType) { case "JUMP_TO_STATE": case "JUMP_TO_ACTION": case "IMPORT_STATE": { const newState = typeof message.state === "string" ? JSON.parse(message.state) : message.state; if (!newState || typeof newState !== "object") return; isTimeTraveling = true; const oldState = localScope.currentState; localScope.currentState = newState; pinia.state[id] = newState; localScope.getterCache.clear(); emit(newState, oldState, []); isTimeTraveling = false; break; } case "COMMIT": { devTools.init(localScope.currentState); break; } case "ROLLBACK": { const newState = typeof message.state === "string" ? JSON.parse(message.state) : message.state; if (!newState || typeof newState !== "object") return; isTimeTraveling = true; const oldState = localScope.currentState; localScope.currentState = newState; pinia.state[id] = newState; localScope.getterCache.clear(); emit(newState, oldState, []); isTimeTraveling = false; break; } case "RESET": { const originalState = options.state(); devTools.init(originalState); internalPatch(() => originalState, "@reset", true); break; } default: break; } } }); } return storePublicApi; } function getStore() { const pinia = getActivePinia(); if (!pinia._s.has(id)) { createStoreInstance(); } if (activeListenerId && activeGetterKey && activeListenerId !== id) { const accessedStoreScope = pinia._scopes.get(id); if (accessedStoreScope) { let subscribers = accessedStoreScope.subscribers.get(activeListenerId); if (!subscribers) { subscribers = /* @__PURE__ */ new Set(); accessedStoreScope.subscribers.set(activeListenerId, subscribers); } subscribers.add(activeGetterKey); } } return pinia._s.get(id); } function useStore() { const pinia = getActivePinia(); if (!pinia._s.has(id)) { createStoreInstance(); } const currentScope = pinia._scopes.get(id); const trackedPaths = useRef(/* @__PURE__ */ new Set()); trackedPaths.current.clear(); const subscribe = useCallback( (onStoreChange) => { const listener = (_state, _prevState, patches) => { let shouldUpdate = false; for (const path of trackedPaths.current) { const topKey = path.split(".")[0]; if (topKey in getters) { if (!currentScope.getterCache.has(topKey)) { shouldUpdate = true; break; } } else { if (patches.length > 0 && isAffected(patches, /* @__PURE__ */ new Set([path]))) { shouldUpdate = true; break; } } } if (shouldUpdate) { onStoreChange(); } }; currentScope.listeners.add(listener); return () => currentScope.listeners.delete(listener); }, [currentScope] ); const getSnapshot = useCallback(() => currentScope.currentState, [currentScope]); useSyncExternalStore(subscribe, getSnapshot, getSnapshot); const trackingProxy = currentScope.createStoreProxy((path) => { trackedPaths.current.add(path.join(".")); }); return trackingProxy; } return { useStore, getStore }; } export { createPinia, defineStore, getActivePinia, setActivePinia }; //# sourceMappingURL=index.js.map