-import { NonNullObject, StateTree, GenericStore } from './types'
+import { InjectionKey, ref, Ref } from '@vue/composition-api'
+import {
+ NonNullObject,
+ StateTree,
+ GenericStore,
+ StoreWithState,
+ StateDescriptor,
+} from './types'
+import Vue, { PluginFunction, VueConstructor } from 'vue'
/**
* setActiveReq must be called to handle SSR at the top of functions like `fetch`, `setup`, `serverPrefetch` and others
* be able to reset the store instance between requests on the server
*/
-export const storesMap = new WeakMap<NonNullObject, Map<string, GenericStore>>()
+export const storesMap = new WeakMap<
+ Pinia,
+ Map<string, [StoreWithState<string, StateTree>, StateDescriptor<StateTree>]>
+>()
/**
* A state provider allows to set how states are stored for hydration. e.g. setting a property on a context, getting a property from window
return rootState
}
+
+// ----------------------------------
+
+/**
+ * Properties that are added to every store by `pinia.use()`
+ */
+// eslint-disable-next-line
+export interface PiniaCustomProperties {}
+
+export const piniaSymbol = (__DEV__
+ ? Symbol('pinia')
+ : Symbol()) as InjectionKey<Pinia>
+
+/**
+ * Plugin to extend every store
+ */
+export interface PiniaStorePlugin {
+ (pinia: Pinia): Partial<PiniaCustomProperties>
+}
+
+/**
+ * Every application must own its own pinia to be able to create stores
+ */
+export interface Pinia {
+ install: PluginFunction<void>
+
+ /**
+ * root state
+ */
+ state: Ref<Record<string, StateTree>>
+
+ /**
+ * Adds a store plugin to extend every store
+ *
+ * @param plugin - store plugin to add
+ */
+ use(plugin: PiniaStorePlugin): void
+
+ /**
+ * Installed store plugins
+ *
+ * @internal
+ */
+ _p: Array<() => Partial<PiniaCustomProperties>>
+
+ /**
+ * Vue constructor retrieved when installing the pinia.
+ */
+ Vue: VueConstructor<Vue>
+}
+
+export const IS_CLIENT = typeof window !== 'undefined'
+
+/**
+ * Creates a Pinia instance to be used by the application
+ */
+export function createPinia(): Pinia {
+ // NOTE: here we could check the window object for a state and directly set it
+ // if there is anything like it with Vue 3 SSR
+ const state = ref({})
+
+ const _p: Pinia['_p'] = []
+ // plugins added before calling app.use(pinia)
+ const toBeInstalled: PiniaStorePlugin[] = []
+
+ const pinia: Pinia = {
+ // this one is set in install
+ Vue: {} as any,
+ install(Vue) {
+ // localApp = app
+ this.Vue = Vue
+
+ // Equivalent of
+ // app.config.globalProperties.$pinia = pinia
+ Vue.mixin({
+ beforeCreate() {
+ const options = this.$options as any
+ // Make pinia accessible everywhere through this.$pinia
+ // FIXME: typings
+ if (options.pinia) {
+ ;(this as any).$pinia = options.pinia
+ // HACK: taken from provide(): https://github.com/vuejs/composition-api/blob/master/src/apis/inject.ts#L25
+ // TODO: check if necessary
+ // if (!(this as any)._provided) {
+ // const provideCache = {}
+ // Object.defineProperty(this, '_provided', {
+ // get: () => provideCache,
+ // set: (v) => Object.assign(provideCache, v),
+ // })
+ // }
+ // ;(this as any)._provided[piniaSymbol as any] = options.pinia
+ } else if (options.parent && options.parent.$pinia) {
+ ;(this as any).$pinia = options.parent.$pinia
+ }
+ },
+ })
+
+ // only set the app on client for devtools
+ if (__BROWSER__ && IS_CLIENT) {
+ // setClientApp(app)
+ // this allows calling useStore() outside of a component setup after
+ // installing pinia's plugin
+ setActivePinia(pinia)
+ }
+
+ toBeInstalled.forEach((plugin) => _p.push(plugin.bind(null, pinia)))
+ },
+
+ use(plugin) {
+ if (!pinia.Vue) {
+ toBeInstalled.push(plugin)
+ } else {
+ _p.push(plugin.bind(null, pinia))
+ }
+ },
+
+ _p,
+
+ state,
+ }
+
+ return pinia
+}
+
+/**
+ * setActivePinia must be called to handle SSR at the top of functions like
+ * `fetch`, `setup`, `serverPrefetch` and others
+ */
+export let activePinia: Pinia | undefined
+
+/**
+ * Sets or unsets the active pinia. Used in SSR and internally when calling
+ * actions and getters
+ *
+ * @param pinia - Pinia instance
+ */
+export const setActivePinia = (pinia: Pinia | undefined) =>
+ (activePinia = pinia)
+
+/**
+ * Get the currently active pinia
+ */
+export const getActivePinia = () => {
+ if (__DEV__ && !activePinia) {
+ console.warn(
+ `[🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia and inject it?\n\n` +
+ `const pinia = createPinia()\n` +
+ `Vue.use(pinia)\n` +
+ `new Vue({ el: '#app', pinia })\n\n` +
+ `This will fail in production.`
+ )
+ }
+
+ return activePinia!
+}
-import { ref, watch, computed, reactive, Ref } from '@vue/composition-api'
+import {
+ watch,
+ computed,
+ reactive,
+ Ref,
+ getCurrentInstance,
+ markRaw,
+} from '@vue/composition-api'
import {
StateTree,
StoreWithState,
Store,
StoreWithActions,
Method,
+ StateDescriptor,
} from './types'
import { useStoreDevtools } from './devtools'
import {
- getActiveReq,
- setActiveReq,
storesMap,
- getInitialState,
+ Pinia,
+ setActivePinia,
+ getActivePinia,
+ PiniaCustomProperties,
} from './rootStore'
+import Vue from 'vue'
const isClient = typeof window != 'undefined'
return target
}
+/**
+ * Create an object of computed properties referring to
+ *
+ * @param rootStateRef - pinia.state
+ * @param id - unique name
+ */
+function computedFromState<T, Id extends string>(
+ rootStateRef: Ref<Record<Id, T>>,
+ id: Id
+) {
+ // let asComputed = computed<T>()
+ const reactiveObject = {} as {
+ [k in keyof T]: Ref<T[k]>
+ }
+ const state = rootStateRef.value[id]
+ for (const key in state) {
+ // @ts-ignore: the key matches
+ reactiveObject[key] = computed({
+ get: () => rootStateRef.value[id][key as keyof T],
+ set: (value) => (rootStateRef.value[id][key as keyof T] = value),
+ })
+ }
+
+ return reactiveObject
+}
+
function toComputed<T>(refObject: Ref<T>) {
// let asComputed = computed<T>()
const reactiveObject = {} as {
}
/**
- * Creates a store instance
- * @param id unique identifier of the store, like a name. eg: main, cart, user
- * @param initialState initial state applied to the store, Must be correctly typed to infer typings
+ * Creates a store with its state object. This is meant to be augmented with getters and actions
+ *
+ * @param id - unique identifier of the store, like a name. eg: main, cart, user
+ * @param buildState - function to build the initial state
+ * @param initialState - initial state applied to the store, Must be correctly typed to infer typings
*/
-export function buildStore<
- Id extends string,
- S extends StateTree,
- G extends Record<string, Method>,
- A extends Record<string, Method>
->(
+function initStore<Id extends string, S extends StateTree>(
$id: Id,
- buildState = () => ({} as S),
- getters: G = {} as G,
- actions: A = {} as A,
+ buildState: () => S = () => ({} as S),
initialState?: S | undefined
-): Store<Id, S, G, A> {
- const $state: Ref<S> = ref(initialState || buildState())
- const _r = getActiveReq()
+): [StoreWithState<Id, S>, { get: () => S; set: (newValue: S) => void }] {
+ const pinia = getActivePinia()
+ pinia.Vue.set(pinia.state.value, $id, initialState || buildState())
+ // const state: Ref<S> = toRef(_p.state.value, $id)
let isListening = true
let subscriptions: SubscriptionCallback<S>[] = []
- watch(
- () => $state.value,
- (state) => {
- if (isListening) {
- subscriptions.forEach((callback) => {
- callback({ storeName: $id, type: '🧩 in place', payload: {} }, state)
- })
- }
- },
- {
- deep: true,
- flush: 'sync',
- }
- )
-
function $patch(partialState: DeepPartial<S>): void {
isListening = false
- innerPatch($state.value, partialState)
+ innerPatch(pinia.state.value[$id], partialState)
isListening = true
// because we paused the watcher, we need to manually call the subscriptions
subscriptions.forEach((callback) => {
callback(
{ storeName: $id, type: '⤵️ patch', payload: partialState },
- $state.value
+ pinia.state.value[$id]
)
})
}
function $subscribe(callback: SubscriptionCallback<S>) {
subscriptions.push(callback)
+
+ // watch here to link the subscription to the current active instance
+ // e.g. inside the setup of a component
+ const stopWatcher = watch(
+ () => pinia.state.value[$id],
+ (state) => {
+ if (isListening) {
+ subscriptions.forEach((callback) => {
+ callback(
+ { storeName: $id, type: '🧩 in place', payload: {} },
+ state
+ )
+ })
+ }
+ },
+ {
+ deep: true,
+ flush: 'sync',
+ }
+ )
+
return () => {
const idx = subscriptions.indexOf(callback)
if (idx > -1) {
subscriptions.splice(idx, 1)
+ stopWatcher()
}
}
}
function $reset() {
subscriptions = []
- $state.value = buildState()
+ pinia.state.value[$id] = buildState()
}
const storeWithState: StoreWithState<Id, S> = {
$id,
- _r,
- // @ts-ignore, `reactive` unwraps this making it of type S
- $state: computed<S>({
- get: () => $state.value,
- set: (newState) => {
- isListening = false
- $state.value = newState
- isListening = true
- },
- }),
+ _p: markRaw(pinia),
+
+ // $state is added underneath
$patch,
$subscribe,
$reset,
- }
+ } as StoreWithState<Id, S>
+
+ return [
+ storeWithState,
+ {
+ get: () => pinia.state.value[$id] as S,
+ set: (newState: S) => {
+ isListening = false
+ pinia.state.value[$id] = newState
+ isListening = true
+ },
+ },
+ ]
+}
+
+/**
+ * Creates a store bound to the lifespan of where the function is called. This
+ * means creating the store inside of a component's setup will bound it to the
+ * lifespan of that component while creating it outside of a component will
+ * create an ever living store
+ *
+ * @param partialStore - store with state returned by initStore
+ * @param descriptor - descriptor to setup $state property
+ * @param $id - unique name of the store
+ * @param getters - getters of the store
+ * @param actions - actions of the store
+ */
+function buildStoreToUse<
+ Id extends string,
+ S extends StateTree,
+ G extends Record<string, Method>,
+ A extends Record<string, Method>
+>(
+ partialStore: StoreWithState<Id, S>,
+ descriptor: StateDescriptor<S>,
+ $id: Id,
+ getters: G = {} as G,
+ actions: A = {} as A
+) {
+ const pinia = getActivePinia()
const computedGetters: StoreWithGetters<G> = {} as StoreWithGetters<G>
for (const getterName in getters) {
computedGetters[getterName] = computed(() => {
- setActiveReq(_r)
- // we could also pass state.value instead of the store as the first
- // argument but the later is more flexible for JS users while TS users
- // will like to use `this` to get all correct typings
+ setActivePinia(pinia)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return getters[getterName].call(store, store)
}) as StoreWithGetters<G>[typeof getterName]
}
- // const reactiveGetters = reactive(computedGetters)
-
const wrappedActions: StoreWithActions<A> = {} as StoreWithActions<A>
for (const actionName in actions) {
wrappedActions[actionName] = function () {
- setActiveReq(_r)
+ setActivePinia(pinia)
// eslint-disable-next-line
return actions[actionName].apply(store, (arguments as unknown) as any[])
} as StoreWithActions<A>[typeof actionName]
}
+ const extensions = pinia._p.reduce(
+ (extended, extender) => ({
+ ...extended,
+ ...extender(),
+ }),
+ {} as PiniaCustomProperties
+ )
+
const store: Store<Id, S, G, A> = reactive({
- ...storeWithState,
+ ...extensions,
+ ...partialStore,
// using this means no new properties can be added as state
- ...toComputed($state),
+ ...computedFromState(pinia.state, $id),
...computedGetters,
...wrappedActions,
}) as Store<Id, S, G, A>
+ // use this instead of a computed with setter to be able to create it anywhere
+ // without linking the computed lifespan to wherever the store is first
+ // created.
+ Object.defineProperty(store, '$state', descriptor)
+
return store
}
}) {
const { id, state, getters, actions } = options
- return function useStore(reqKey?: object): Store<Id, S, G, A> {
- if (reqKey) setActiveReq(reqKey)
- const req = getActiveReq()
- let stores = storesMap.get(req)
- if (!stores) storesMap.set(req, (stores = new Map()))
+ return function useStore(pinia?: Pinia | null): Store<Id, S, G, A> {
+ const vm = getCurrentInstance()
+ pinia = pinia || (vm && ((vm as any).$pinia as Pinia))
+
+ if (pinia) setActivePinia(pinia)
+
+ pinia = getActivePinia()
+ let stores = storesMap.get(pinia)
+ if (!stores) storesMap.set(pinia, (stores = new Map()))
- let store = stores.get(id) as Store<Id, S, G, A>
- if (!store) {
- stores.set(
+ // let store = stores.get(id) as Store<Id, S, G, A>
+ let storeAndDescriptor = stores.get(id) as
+ | [StoreWithState<Id, S>, StateDescriptor<S>]
+ | undefined
+
+ if (!storeAndDescriptor) {
+ storeAndDescriptor = initStore(id, state, pinia.state.value[id])
+
+ stores.set(id, storeAndDescriptor)
+
+ const store = buildStoreToUse(
+ storeAndDescriptor[0],
+ storeAndDescriptor[1],
id,
- // @ts-ignore
- (store = buildStore(id, state, getters, actions, getInitialState(id)))
+ getters as Record<string, Method> | undefined,
+ actions as Record<string, Method> | undefined
)
if (isClient) useStoreDevtools(store)
+
+ return store
}
- return store
+ return buildStoreToUse(
+ storeAndDescriptor[0],
+ storeAndDescriptor[1],
+ id,
+ getters as Record<string, Method> | undefined,
+ actions as Record<string, Method> | undefined
+ )
}
}