]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat(store): function wip
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 8 Jul 2021 16:42:50 +0000 (18:42 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 19 Jul 2021 09:51:12 +0000 (11:51 +0200)
src/createPinia.ts
src/mapHelpers.ts
src/rootStore.ts
src/store.ts
src/types.ts

index 587928aaf4775a30aee6dbe54fc76493fe194f16..aaaee6fd4542ef5a463fb6f8a17b506680185afc 100644 (file)
@@ -4,17 +4,19 @@ import {
   setActivePinia,
   piniaSymbol,
 } from './rootStore'
-import { ref, App, markRaw } from 'vue'
+import { ref, App, markRaw, effectScope } from 'vue'
 import { devtoolsPlugin } from './devtools'
 import { IS_CLIENT } from './env'
+import { StateTree, Store } from './types'
 
 /**
  * Creates a Pinia instance to be used by the application
  */
 export function createPinia(): Pinia {
+  const scope = effectScope(true)
   // 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 state = scope.run(() => ref<Record<string, StateTree>>({}))!
 
   let localApp: App | undefined
   let _p: Pinia['_p'] = []
@@ -46,7 +48,8 @@ export function createPinia(): Pinia {
     _p,
     // it's actually undefined here
     _a: localApp!,
-
+    _e: scope,
+    _s: new Map<string, Store>(),
     state,
   })
 
index 0a6f1ff7d83bb8c85d9bd82b098d48fc711c1295..47f976773c7db242b8a0ab5d34235ae8c16e596a 100644 (file)
@@ -221,6 +221,7 @@ export function mapState<
   useStore: StoreDefinition<Id, S, G, A>,
   keyMapper: KeyMapper
 ): _MapStateObjectReturn<Id, S, G, A, KeyMapper>
+
 /**
  * Allows using state and getters from one store without using the composition
  * API (`setup()`) by generating an object to be spread in the `computed` field
@@ -253,6 +254,7 @@ export function mapState<
   useStore: StoreDefinition<Id, S, G, A>,
   keys: Array<keyof S | keyof G>
 ): _MapStateReturn<S, G>
+
 /**
  * Allows using state and getters from one store without using the composition
  * API (`setup()`) by generating an object to be spread in the `computed` field
index 64eb0396cb60222eda30428b62f0afe9c4ee26e5..cd6b08353f67ad1e77f40338641a83e32183237e 100644 (file)
@@ -1,4 +1,4 @@
-import { App, InjectionKey, Plugin, Ref, warn } from 'vue'
+import { App, EffectScope, InjectionKey, Plugin, Ref, warn } from 'vue'
 import {
   StateTree,
   StoreWithState,
@@ -10,6 +10,7 @@ import {
   GettersTree,
   ActionsTree,
   PiniaCustomStateProperties,
+  GenericStore,
 } from './types'
 
 /**
@@ -92,6 +93,19 @@ export interface Pinia {
    */
   _a: App
 
+  /**
+   * Effect scope the pinia is attached to
+   *
+   * @internal
+   */
+  _e: EffectScope
+
+  /**
+   *
+   * @internal
+   */
+  _s: Map<string, GenericStore>
+
   /**
    * Added by `createTestingPinia()` to bypass `useStore(pinia)`.
    *
index 82218b1664020fabc543f43a3c8d6ade3b7700d1..cef3dc9eab6c8d88f382f7005a0e1e46ec338908 100644 (file)
@@ -5,7 +5,6 @@ import {
   inject,
   getCurrentInstance,
   reactive,
-  onUnmounted,
   InjectionKey,
   provide,
   DebuggerEvent,
@@ -14,6 +13,13 @@ import {
   markRaw,
   isRef,
   isReactive,
+  effectScope,
+  onScopeDispose,
+  EffectScope,
+  getCurrentScope,
+  onUnmounted,
+  ComputedRef,
+  toRef,
 } from 'vue'
 import {
   StateTree,
@@ -34,6 +40,7 @@ import {
   UnwrapPromise,
   ActionsTree,
   SubscriptionCallbackMutation,
+  _UnionToTuple,
 } from './types'
 import {
   getActivePinia,
@@ -122,6 +129,7 @@ function initStore<
   pinia.state.value[$id] = initialState || buildState()
   // const state: Ref<S> = toRef(_p.state.value, $id)
 
+  // internal state
   let isListening = true
   let subscriptions: SubscriptionCallback<S>[] = markRaw([])
   let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = markRaw([])
@@ -392,8 +400,429 @@ function buildStoreToUse<
   return store
 }
 
+type _SetupOfStateActions<
+  S extends StateTree,
+  A extends Record<any, _Method>
+> = (StateTree extends S ? {} : S) & (Record<any, _Method> extends A ? {} : A)
+
+function stateBuilder<S extends StateTree>(
+  builder: (initial?: S | undefined) => S
+) {
+  return {} as S
+}
+
+const a = stateBuilder<{ n: number; toggle: boolean }>((initial) => ({
+  n: initial?.n || 2,
+  toggle: true,
+}))
+
+export interface DefineSetupStoreOptions<
+  Id extends string,
+  S extends StateTree,
+  G extends ActionsTree, // TODO: naming
+  A extends ActionsTree
+> {
+  hydrate?(store: Store<Id, S, G, A>, initialState: S | undefined): void
+}
+
+function isComputed(o: any) {
+  return o && o.effect && o.effect.computed
+}
+
+function createSetupStore<
+  Id extends string,
+  SS,
+  S extends StateTree,
+  G extends ActionsTree, // TODO: naming
+  A extends ActionsTree
+>(
+  $id: Id,
+  setup: () => SS,
+  initialState: S | undefined,
+  {
+    // @ts-expect-error
+    hydrate = innerPatch,
+  }: DefineSetupStoreOptions<Id, S, G, A> = {}
+): Store<Id, S, {}, A> {
+  const pinia = getActivePinia()
+  let scope!: EffectScope
+
+  // watcher options for $subscribe
+  const $subscribeOptions: WatchOptions = { deep: true, flush: 'sync' }
+  /* istanbul ignore else */
+  if (__DEV__) {
+    $subscribeOptions.onTrigger = (event) => {
+      if (isListening) {
+        debuggerEvents = event
+      } else {
+        // let patch send all the events together later
+        /* istanbul ignore else */
+        if (Array.isArray(debuggerEvents)) {
+          debuggerEvents.push(event)
+        } else {
+          console.error(
+            '🍍 debuggerEvents should be an array. This is most likely an internal Pinia bug.'
+          )
+        }
+      }
+    }
+  }
+
+  // internal state
+  let isListening = true
+  let subscriptions: SubscriptionCallback<S>[] = markRaw([])
+  let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = markRaw([])
+  let debuggerEvents: DebuggerEvent[] | DebuggerEvent
+
+  const triggerSubscriptions: SubscriptionCallback<S> = (mutation, state) => {
+    subscriptions.forEach((callback) => {
+      callback(mutation, state)
+    })
+  }
+
+  if (__DEV__ && !pinia._e.active) {
+    // TODO: warn in dev
+    throw new Error('Pinia destroyed')
+  }
+
+  // TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
+  const setupStore = pinia._e.run(() => {
+    scope = effectScope()
+    return scope.run(() => {
+      const store = setup()
+      // TODO: extract state and set it to pinia.state.value[$id]
+      // pinia.state.value[$id] = initialState || buildState()
+
+      watch(
+        () => pinia.state.value[$id] as UnwrapRef<S>,
+        (state, oldState) => {
+          if (isListening) {
+            triggerSubscriptions(
+              {
+                storeId: $id,
+                type: MutationType.direct,
+                events: debuggerEvents as DebuggerEvent,
+              },
+              state
+            )
+          }
+        },
+        $subscribeOptions
+      )!
+
+      return store
+    })
+  })!
+
+  function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
+  function $patch(partialState: DeepPartial<UnwrapRef<S>>): void
+  function $patch(
+    partialStateOrMutator:
+      | DeepPartial<UnwrapRef<S>>
+      | ((state: UnwrapRef<S>) => void)
+  ): void {
+    let subscriptionMutation: SubscriptionCallbackMutation<S>
+    isListening = false
+    // reset the debugger events since patches are sync
+    /* istanbul ignore else */
+    if (__DEV__) {
+      debuggerEvents = []
+    }
+    if (typeof partialStateOrMutator === 'function') {
+      partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
+      subscriptionMutation = {
+        type: MutationType.patchFunction,
+        storeId: $id,
+        events: debuggerEvents as DebuggerEvent[],
+      }
+    } else {
+      innerPatch(pinia.state.value[$id], partialStateOrMutator)
+      subscriptionMutation = {
+        type: MutationType.patchObject,
+        payload: partialStateOrMutator,
+        storeId: $id,
+        events: debuggerEvents as DebuggerEvent[],
+      }
+    }
+    isListening = true
+    // because we paused the watcher, we need to manually call the subscriptions
+    triggerSubscriptions(
+      subscriptionMutation,
+      pinia.state.value[$id] as UnwrapRef<S>
+    )
+  }
+
+  function $subscribe(callback: SubscriptionCallback<S>, detached?: boolean) {
+    subscriptions.push(callback)
+
+    if (!detached) {
+      if (getCurrentInstance()) {
+        onUnmounted(() => {
+          const idx = subscriptions.indexOf(callback)
+          if (idx > -1) {
+            subscriptions.splice(idx, 1)
+          }
+        })
+      }
+    }
+  }
+
+  function $onAction(callback: StoreOnActionListener<Id, S, G, A>) {
+    actionSubscriptions.push(callback)
+
+    const removeSubscription = () => {
+      const idx = actionSubscriptions.indexOf(callback)
+      if (idx > -1) {
+        actionSubscriptions.splice(idx, 1)
+      }
+    }
+
+    if (getCurrentInstance()) {
+      onScopeDispose(removeSubscription)
+    }
+
+    return removeSubscription
+  }
+
+  function $reset() {
+    // TODO: is it worth? probably should be removed
+    // maybe it can stop the effect and create it again
+    // pinia.state.value[$id] = buildState()
+  }
+
+  // overwrite existing actions to support $onAction
+  for (const key in setupStore) {
+    const prop = setupStore[key]
+
+    // action
+    if (typeof prop === 'function') {
+      // @ts-expect-error: we are overriding the function
+      setupStore[key] = function () {
+        setActivePinia(pinia)
+        const args = Array.from(arguments)
+
+        let afterCallback: (resolvedReturn: any) => void = noop
+        let onErrorCallback: (error: unknown) => void = noop
+        function after(callback: typeof afterCallback) {
+          afterCallback = callback
+        }
+        function onError(callback: typeof onErrorCallback) {
+          onErrorCallback = callback
+        }
+
+        actionSubscriptions.forEach((callback) => {
+          // @ts-expect-error
+          callback({
+            args,
+            name: key,
+            store,
+            after,
+            onError,
+          })
+        })
+
+        let ret: any
+        try {
+          ret = prop.apply(this, args)
+          Promise.resolve(ret).then(afterCallback).catch(onErrorCallback)
+        } catch (error) {
+          onErrorCallback(error)
+          throw error
+        }
+
+        return ret
+      }
+    } else if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
+      // mark it as a piece of state to be serialized
+      pinia.state.value[$id] = toRef(setupStore as any, key)
+    } else if (__DEV__ && IS_CLIENT) {
+      // add getters for devtools
+      if (isComputed(prop)) {
+        const getters: string[] =
+          // @ts-expect-error: it should be on the store
+          setupStore._getters || (setupStore._getters = markRaw([]))
+        getters.push(key)
+      }
+    }
+  }
+
+  const partialStore = {
+    $id,
+    $onAction,
+    $patch,
+    $reset,
+    $subscribe,
+  }
+
+  const store: Store<Id, S, G, A> = reactive(
+    assign(
+      __DEV__ && IS_CLIENT
+        ? // devtools custom properties
+          {
+            _customProperties: markRaw(new Set<string>()),
+          }
+        : {},
+      partialStore,
+      setupStore
+    )
+  ) 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', {
+    get: () => pinia.state.value[$id],
+    set: (state) => (pinia.state.value[$id] = state),
+  })
+
+  // apply all plugins
+  pinia._p.forEach((extender) => {
+    if (__DEV__ && IS_CLIENT) {
+      // @ts-expect-error: conflict between A and ActionsTree
+      // TODO: completely different options...
+      const extensions = extender({ store, app: pinia._a, pinia, options })
+      Object.keys(extensions || {}).forEach((key) =>
+        store._customProperties.add(key)
+      )
+      assign(store, extensions)
+    } else {
+      // @ts-expect-error: conflict between A and ActionsTree
+      assign(store, extender({ store, app: pinia._a, pinia, options }))
+    }
+  })
+
+  return store
+}
+
+// const useStore = createSetupStore('cosa', () => {
+//   return {
+//     o: 'one',
+//   }
+// })
+
+type _SpreadStateFromStore<SS, K extends readonly any[]> = K extends readonly [
+  infer A,
+  ...infer Rest
+]
+  ? A extends string | number | symbol
+    ? SS extends Record<A, _Method | ComputedRef<any>>
+      ? _SpreadStateFromStore<SS, Rest>
+      : SS extends Record<A, any>
+      ? Record<A, UnwrapRef<SS[A]>> & _SpreadStateFromStore<SS, Rest>
+      : never
+    : {}
+  : {}
+
+type _SpreadPropertiesFromObject<
+  SS,
+  K extends readonly any[],
+  T
+> = K extends readonly [infer A, ...infer Rest]
+  ? A extends string | number | symbol
+    ? SS extends Record<A, T>
+      ? Record<A, UnwrapRef<SS[A]>> & _SpreadPropertiesFromObject<SS, Rest, T>
+      : _SpreadPropertiesFromObject<SS, Rest, T>
+    : {}
+  : {}
+
+type _ExtractStateFromSetupStore<SS> = _SpreadStateFromStore<
+  SS,
+  _UnionToTuple<keyof SS>
+>
+
+type _ExtractActionsFromSetupStore<SS> = _SpreadPropertiesFromObject<
+  SS,
+  _UnionToTuple<keyof SS>,
+  _Method
+>
+
+type _ExtractGettersFromSetupStore<SS> = _SpreadPropertiesFromObject<
+  SS,
+  _UnionToTuple<keyof SS>,
+  ComputedRef<any>
+>
+
+// type a1 = _ExtractStateFromSetupStore<{ a: Ref<number>; action: () => void }>
+// type a2 = _ExtractActionsFromSetupStore<{ a: Ref<number>; action: () => void }>
+// type a3 = _ExtractGettersFromSetupStore<{
+//   a: Ref<number>
+//   b: ComputedRef<string>
+//   action: () => void
+// }>
+
+/**
+ * Creates a `useStore` function that retrieves the store instance
+ *
+ * @param options - options to define the store
+ */
+export function defineSetupStore<Id extends string, SS>(
+  id: Id,
+  storeSetup: () => SS,
+  options?: DefineSetupStoreOptions<
+    Id,
+    _ExtractStateFromSetupStore<SS>,
+    _ExtractGettersFromSetupStore<SS>,
+    _ExtractActionsFromSetupStore<SS>
+  >
+): StoreDefinition<
+  Id,
+  _ExtractStateFromSetupStore<SS>,
+  _ExtractGettersFromSetupStore<SS>,
+  _ExtractActionsFromSetupStore<SS>
+> {
+  function useStore(
+    pinia?: Pinia | null
+  ): Store<
+    Id,
+    _ExtractStateFromSetupStore<SS>,
+    _ExtractGettersFromSetupStore<SS>,
+    _ExtractActionsFromSetupStore<SS>
+  > {
+    const currentInstance = getCurrentInstance()
+    pinia =
+      // in test mode, ignore the argument provided as we can always retrieve a
+      // pinia instance with getActivePinia()
+      (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
+      (currentInstance && inject(piniaSymbol))
+    if (pinia) setActivePinia(pinia)
+    // TODO: worth warning on server if no piniaKey as it can leak data
+    pinia = getActivePinia()
+
+    if (!pinia._s.has(id)) {
+      pinia._s.set(id, createSetupStore(id, storeSetup, options))
+    }
+
+    const store: Store<
+      Id,
+      _ExtractStateFromSetupStore<SS>,
+      _ExtractGettersFromSetupStore<SS>,
+      _ExtractActionsFromSetupStore<SS>
+    > = pinia._s.get(id)! as Store<
+      Id,
+      _ExtractStateFromSetupStore<SS>,
+      _ExtractGettersFromSetupStore<SS>,
+      _ExtractActionsFromSetupStore<SS>
+    >
+
+    // save stores in instances to access them devtools
+    if (__DEV__ && IS_CLIENT && currentInstance && currentInstance.proxy) {
+      const vm = currentInstance.proxy
+      const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
+      // @ts-expect-error: still can't cast Store with generics to Store
+      cache[id] = store
+    }
+
+    return store
+  }
+
+  useStore.$id = id
+
+  return useStore
+}
+
 /**
  * Creates a `useStore` function that retrieves the store instance
+ *
  * @param options - options to define the store
  */
 export function defineStore<
index 456e3e37daa6554c2ac73b34dc85f21d03f60856..3598bdacf183ac9b223c5378e6b50a53c857d2fe 100644 (file)
@@ -500,3 +500,34 @@ export interface DefineStoreOptions<
         PiniaCustomProperties
     >
 }
+
+export type _UnionToTuple<U> = _UnionToTupleRecursively<[], U>
+
+type _Overwrite<T, S extends any> = {
+  [P in keyof T]: P extends keyof S ? S[P] : never
+}
+type _TupleUnshift<T extends any[], X> = T extends any
+  ? ((x: X, ...t: T) => void) extends (...t: infer R) => void
+    ? R
+    : never
+  : never
+type TuplePush<T extends any[], X> = T extends any
+  ? _Overwrite<_TupleUnshift<T, any>, T & { [x: string]: X }>
+  : never
+type _UnionToIntersection<U> = (
+  U extends any ? (k: U) => void : never
+) extends (k: infer I) => void
+  ? I
+  : never
+type _UnionToOvlds<U> = _UnionToIntersection<
+  U extends any ? (f: U) => void : never
+>
+type _PopUnion<U> = _UnionToOvlds<U> extends (a: infer A) => void ? A : never
+/* end helpers */
+/* main work */
+type _UnionToTupleRecursively<T extends any[], U> = {
+  1: T
+  0: _PopUnion<U> extends infer SELF
+    ? _UnionToTupleRecursively<TuplePush<T, SELF>, Exclude<U, SELF>>
+    : never
+}[[U] extends [never] ? 1 : 0]