Skip to main content
Glama
pinia_hooks_plugin.ts6.22 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { PiniaPlugin, PiniaPluginContext } from "pinia"; import { ComponentInternalInstance, computed, getCurrentInstance, reactive, } from "vue"; import isPromise from "is-promise"; import * as _ from "lodash-es"; type MaybePromise<T> = T | Promise<T>; declare module "pinia" { /* eslint-disable @typescript-eslint/no-unused-vars */ export interface DefineStoreOptionsBase<S, Store> { // adds our new custom option for activation/deactivation hook onActivated?: (this: Store) => MaybePromise<void | (() => void)>; // adds our new custom option for hook that first on first use/activation only onInit?: (this: Store) => MaybePromise<void>; } export interface PiniaCustomStateProperties<S> { trackStoreUsedByComponent(component: ComponentInternalInstance): void; } } // TODO: couldnt get the typing of T happy here... but it works for consumers export function addStoreHooks<T extends () => any>( workspaceId: string | undefined | null, changeSetId: string | undefined | null, useStoreFn: T, ) { return (...args: Parameters<T>): ReturnType<T> => { const store = useStoreFn.apply(null, [...args]) as ReturnType<T>; const component = getCurrentInstance(); if (component) store.trackStoreUsedByComponent(component); store.workspaceId = workspaceId; store.changeSetId = changeSetId; return store; }; } export const piniaHooksPlugin: PiniaPlugin = ({ // pinia, // app, store, options: storeOptions, }: PiniaPluginContext) => { if (store.$id === "heimdall") return; /* eslint-disable no-param-reassign */ // might not need this check, but not sure this plugin code is guaranteed to only be called once if (store._trackedStoreUsers) return; store._initHookCalled = false; // keep a list of all components using this store store._trackedStoreUsers = reactive<Record<string, boolean>>({}); store._trackedStoreUsersCount = computed( () => Object.keys(store._trackedStoreUsers).length, ); // TODO: handle reset logic - see https://pinia.vuejs.org/core-concepts/plugins.html#Resetting-state-added-in-plugins // expose this info to devtools // TODO: determine the best way to safely check in both vite and webpack setups if (import.meta.env.DEV /* || process.env.NODE_ENV === "development" */) { store._customProperties.add("_trackedStoreUsers"); store._customProperties.add("_trackedStoreUsersCount"); } function trackStoreUse( component: ComponentInternalInstance, trackedComponentId: string, ) { // bail if already tracked - which can happen when stores are using each other in getters if (store._trackedStoreUsers[trackedComponentId]) return; // console.log("track store use", trackedComponentId); store._trackedStoreUsers[trackedComponentId] = true; if (!store._initHookCalled && storeOptions.onInit) { // TODO: what to do if this errors? // eslint-disable-next-line @typescript-eslint/no-floating-promises storeOptions.onInit.call(store); } // console.log(store.$id, "+"); // store._trackedStoreUsersCount++; if (store._trackedStoreUsersCount === 1 && storeOptions.onActivated) { // console.log(`${store.$id} - ACTIVATE`); // activation fn can return a deactivate / cleanup fn store.onDeactivated = storeOptions.onActivated.call(store); // activate could be async, so need to resolve if so... // TODO may need to think more about this - what if activate errors out? if (isPromise(store.onDeactivated)) { // eslint-disable-next-line @typescript-eslint/no-floating-promises store.onDeactivated.then((resolvedOnDeactivate) => { store.onDeactivated = resolvedOnDeactivate; }); } } // attach the the unmounted hook here so it only ever gets added once // (because we bailed above if this component was already tracked) const componentAny = component as any; // onBeforeUnmount(() => { store.unmarkStoreUsedByComponent(); }); componentAny.bum = componentAny.bum || []; componentAny.bum.push(() => { // console.log(`[${store.$id}] -- ${trackedComponentId} un-used`); if (!store._trackedStoreUsers[trackedComponentId]) { throw new Error( `[${store.$id}] Expected to find component ${trackedComponentId} in list of users`, ); } delete store._trackedStoreUsers[trackedComponentId]; if ( store._trackedStoreUsersCount === 0 && store.onDeactivated && _.isFunction(store.onDeactivated) ) { // console.log(`${store.$id} - DEACTIVATE`); store.onDeactivated.call(store); } }); } store.trackStoreUsedByComponent = (component: ComponentInternalInstance) => { // console.log( // `[${store.$id}] track use by ${component.type.__name} -- mounted? ${component.isMounted}`, // component, // ); // calling lifecycle hooks here (ie beforeMount) causes problems for useStore calls within other store getters/actions // so we're injecting the hooks directly into the vue component instance // this is probably inadvisable... but seems to work and I don't believe this will change any time soon // as an added bonus, this means `watch` calls in our onActivated hook are not bound to the first component that used this store // so they will not be destroyed on unmount. This requires us to clean up after ourselves, but it is the desired behaviour. // onBeforeMount(() => { store.markStoreUsedByComponent(); }); const componentIdForUseTracking = `${component.type.__name}/${component.uid}`; const componentAny = component as any; // console.log( // `tracking ${componentIdForUseTracking}`, // JSON.stringify(store._trackedStoreUsers), // ); if (component.isMounted) { trackStoreUse(component, componentIdForUseTracking); } else { // note - this can happen multiple times, but we handle this case in `trackStoreUse()` componentAny.m = componentAny.m || []; componentAny.m.push(() => { trackStoreUse(component, componentIdForUseTracking); }); } }; };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server