]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
refactor(subscribe): better types
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 13 May 2021 09:37:18 +0000 (11:37 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 13 May 2021 11:31:55 +0000 (13:31 +0200)
__tests__/lifespan.spec.ts
__tests__/store.spec.ts
__tests__/subscriptions.spec.ts
src/index.ts
src/store.ts
src/types.ts

index 36ae2ef8a48c955f05b35de7862e39eb01504a84..44d308e0382e5e6f3850294d3084b070ed5d1a7a 100644 (file)
@@ -107,7 +107,8 @@ describe('Store Lifespan', () => {
     expect(inComponentWatch).toHaveBeenCalledTimes(2)
   })
 
-  it('ref in state reactivity outlives component life', async () => {
+  // FIXME: same limitation as above
+  it.skip('ref in state reactivity outlives component life', async () => {
     let n: Ref<number>
     const globalWatch = jest.fn()
     const destroy = watch(() => pinia.state.value.a?.n, globalWatch)
index 1a1935d87f27ddbdbe60aca3641391ebd9baa622..b21c4af6d5891e1913f7aa35061e5a0bafc72263 100644 (file)
@@ -1,6 +1,9 @@
-import { defineComponent } from '@vue/composition-api'
+import {
+  defineComponent,
+  getCurrentInstance,
+  watch,
+} from '@vue/composition-api'
 import { createLocalVue, mount } from '@vue/test-utils'
-import { MutationType } from '../src'
 import Vue from 'vue'
 import {
   createPinia,
@@ -129,39 +132,80 @@ describe('Store', () => {
     expect(store2.$state.nested.a.b).toBe('string')
   })
 
-  it('subscribe to changes', () => {
-    const store = useStore()
-    const spy = jest.fn()
-    store.$subscribe(spy)
-
-    store.$state.a = false
+  it('should outlive components', async () => {
+    const pinia = createPinia()
+    pinia.Vue = Vue
+    const localVue = createLocalVue()
+    localVue.use(PiniaPlugin)
+    const useStore = defineStore({
+      id: 'main',
+      state: () => ({ n: 0 }),
+    })
 
-    expect(spy).toHaveBeenCalledWith(
+    const wrapper = mount(
       {
-        payload: {},
-        storeName: 'main',
-        type: MutationType.direct,
+        setup() {
+          const store = useStore()
+
+          return { store }
+        },
+
+        template: `<p>n: {{ store.n }}</p>`,
       },
-      store.$state
+      {
+        pinia,
+        localVue,
+      }
     )
-  })
 
-  it('subscribe to changes done via patch', () => {
-    const store = useStore()
+    expect(wrapper.text()).toBe('n: 0')
+
+    const store = useStore(pinia)
+
     const spy = jest.fn()
-    store.$subscribe(spy)
+    watch(() => store.n, spy)
+
+    expect(spy).toHaveBeenCalledTimes(0)
+    store.n++
+    await wrapper.vm.$nextTick()
+    expect(spy).toHaveBeenCalledTimes(1)
+    expect(wrapper.text()).toBe('n: 1')
 
-    const patch = { a: false }
-    store.$patch(patch)
+    await wrapper.destroy()
+    store.n++
+    await wrapper.vm.$nextTick()
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
 
-    expect(spy).toHaveBeenCalledWith(
+  it('should not break getCurrentInstance', () => {
+    const pinia = createPinia()
+    pinia.Vue = Vue
+    const localVue = createLocalVue()
+    localVue.use(PiniaPlugin)
+    const useStore = defineStore({
+      id: 'other',
+      state: () => ({ a: true }),
+    })
+    let store: ReturnType<typeof useStore> | undefined
+
+    let i1: any = {}
+    let i2: any = {}
+    const wrapper = mount(
       {
-        payload: patch,
-        storeName: 'main',
-        type: MutationType.patchObject,
+        setup() {
+          i1 = getCurrentInstance()
+          store = useStore()
+          i2 = getCurrentInstance()
+
+          return { store }
+        },
+
+        template: `<p>a: {{ store.a }}</p>`,
       },
-      store.$state
+      { pinia, localVue }
     )
+
+    expect(i1).toBe(i2)
   })
 
   it('reuses stores from parent components', () => {
index 3277360de481f1a7ff64049a28816c37dc745401..5ed4e305d06fab4c383aac2e441e476c909e404b 100644 (file)
@@ -7,6 +7,7 @@ import {
   createPinia,
   Pinia,
   PiniaPlugin,
+  MutationType,
 } from '../src'
 
 describe('Subscriptions', () => {
@@ -34,6 +35,33 @@ describe('Subscriptions', () => {
     store.$subscribe(spy)
     store.$state.name = 'Cleiton'
     expect(spy).toHaveBeenCalledTimes(1)
+    expect(spy).toHaveBeenCalledWith(
+      expect.objectContaining({
+        storeName: 'main',
+        storeId: 'main',
+        type: MutationType.direct,
+      }),
+      store.$state
+    )
+  })
+
+  it('subscribe to changes done via patch', () => {
+    const store = useStore()
+    const spy = jest.fn()
+    store.$subscribe(spy)
+
+    const patch = { name: 'Cleiton' }
+    store.$patch(patch)
+
+    expect(spy).toHaveBeenCalledWith(
+      expect.objectContaining({
+        payload: patch,
+        storeName: 'main',
+        storeId: 'main',
+        type: MutationType.patchObject,
+      }),
+      store.$state
+    )
   })
 
   it('unsubscribes callback when unsubscribe is called', () => {
index 21f39f14a07b172233c34eac540a63d05b8efd8c..a8da3e30831e4d6905550411517386c8c487de20 100644 (file)
@@ -15,6 +15,7 @@ export type {
   StoreWithState,
   StoreOnActionListener,
   StoreOnActionListenerContext,
+  SubscriptionCallback,
   PiniaCustomProperties,
   DefineStoreOptions,
 } from './types'
index 4c4794a69d71e7e2094a845282a58ca610cb4a8e..535baa43e07e0b53989f29008fd7bef2a0ade584 100644 (file)
@@ -25,9 +25,10 @@ import {
   StoreDefinition,
   GettersTree,
   DefineStoreOptions,
-  GenericStore,
   StoreOnActionListener,
   MutationType,
+  ActionsTree,
+  SubscriptionCallbackMutation,
 } from './types'
 import { useStoreDevtools } from './devtools'
 import {
@@ -115,24 +116,28 @@ function initStore<Id extends string, S extends StateTree>(
   function $patch(
     partialStateOrMutator: DeepPartial<S> | ((state: S) => void)
   ): void {
-    let partialState: DeepPartial<S> = {}
-    let type: MutationType
+    let subscriptionMutation: SubscriptionCallbackMutation<S>
     isListening = false
     if (typeof partialStateOrMutator === 'function') {
       partialStateOrMutator(pinia.state.value[$id])
-      type = MutationType.patchFunction
+      subscriptionMutation = {
+        type: MutationType.patchFunction,
+        storeName: $id,
+        storeId: $id,
+      }
     } else {
       innerPatch(pinia.state.value[$id], partialStateOrMutator)
-      partialState = partialStateOrMutator
-      type = MutationType.patchObject
+      subscriptionMutation = {
+        type: MutationType.patchObject,
+        payload: partialStateOrMutator,
+        storeName: $id,
+        storeId: $id,
+      }
     }
     isListening = true
     // because we paused the watcher, we need to manually call the subscriptions
     subscriptions.forEach((callback) => {
-      callback(
-        { storeName: $id, type, payload: partialState },
-        pinia.state.value[$id] as UnwrapRef<S>
-      )
+      callback(subscriptionMutation, pinia.state.value[$id] as UnwrapRef<S>)
     })
   }
 
@@ -146,7 +151,11 @@ function initStore<Id extends string, S extends StateTree>(
       (state) => {
         if (isListening) {
           callback(
-            { storeName: $id, type: MutationType.direct, payload: {} },
+            {
+              storeName: $id,
+              storeId: $id,
+              type: MutationType.direct,
+            },
             state
           )
         }
index dd097368168f2d686097c5546c7a7e59247d69d1..7ccdca1a261792bc89842ccf44330b752c88a976 100644 (file)
@@ -59,6 +59,93 @@ export enum MutationType {
   // maybe reset? for $state = {} and $reset
 }
 
+/**
+ * Base type for the context passed to a subscription callback.
+ *
+ * @internal
+ */
+export interface _SubscriptionCallbackMutationBase {
+  /**
+   * Type of the mutation.
+   */
+  type: MutationType
+
+  /**
+   * @deprecated use `storeId` instead.
+   */
+  storeName: string
+
+  /**
+   * `id` of the store doing the mutation.
+   */
+  storeId: string
+}
+
+/**
+ * Context passed to a subscription callback when directly mutating the state of
+ * a store with `store.someState = newValue` or `store.$state.someState =
+ * newValue`.
+ */
+export interface SubscriptionCallbackMutationDirect
+  extends _SubscriptionCallbackMutationBase {
+  type: MutationType.direct
+}
+
+/**
+ * Context passed to a subscription callback when `store.$patch()` is called
+ * with an object.
+ */
+export interface SubscriptionCallbackMutationPatchObject<S>
+  extends _SubscriptionCallbackMutationBase {
+  type: MutationType.patchObject
+
+  /**
+   * Object passed to `store.$patch()`.
+   */
+  payload: DeepPartial<S>
+}
+
+/**
+ * Context passed to a subscription callback when `store.$patch()` is called
+ * with a function.
+ */
+export interface SubscriptionCallbackMutationPatchFunction
+  extends _SubscriptionCallbackMutationBase {
+  type: MutationType.patchFunction
+
+  /**
+   * Object passed to `store.$patch()`.
+   */
+  // payload: DeepPartial<UnwrapRef<S>>
+}
+
+/**
+ * Context object passed to a subscription callback.
+ */
+export type SubscriptionCallbackMutation<S> =
+  | SubscriptionCallbackMutationDirect
+  | SubscriptionCallbackMutationPatchObject<S>
+  | SubscriptionCallbackMutationPatchFunction
+
+export type UnwrapPromise<T> = T extends Promise<infer V> ? V : T
+
+/**
+ * Callback of a subscription
+ */
+export type SubscriptionCallback<S> = (
+  /**
+   * Object with information relative to the store mutation that triggered the
+   * subscription.
+   */
+  mutation: SubscriptionCallbackMutation<S>,
+
+  /**
+   * State of the store when the subscription is triggered. Same as
+   * `store.$state`.
+   */
+  state: UnwrapRef<S>
+) => void
+
 /**
  * Context object passed to callbacks of `store.$onAction(context => {})`
  */
@@ -98,21 +185,6 @@ export type StoreOnActionListener = (
   context: StoreOnActionListenerContext
 ) => void
 
-/**
- * Callback of a subscription
- */
-export type SubscriptionCallback<S> = (
-  // TODO: make type an enumeration
-  // TODO: payload should be optional
-  mutation: {
-    storeName: string
-    type: MutationType
-
-    payload: DeepPartial<UnwrapRef<S>>
-  },
-  state: UnwrapRef<S>
-) => void
-
 /**
  * Base store with state and functions
  * @internal
@@ -165,14 +237,9 @@ export interface StoreWithState<Id extends string, S extends StateTree> {
    * cleanup up when the component gets unmounted.
    *
    * @param callback - callback passed to the watcher
-   * @param onTrigger - DEV ONLY watcher debugging
-   * (https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watcher-debugging)
    * @returns function that removes the watcher
    */
-  $subscribe(
-    callback: SubscriptionCallback<S>,
-    onTrigger?: (event: any) => void
-  ): () => void
+  $subscribe(callback: SubscriptionCallback<S>): () => void
 
   /**
    * Array of registered action subscriptions.
@@ -263,11 +330,11 @@ export type StoreWithGetters<G> = {
  * Store type to build a store
  */
 export type Store<
-  Id extends string,
-  S extends StateTree,
-  G extends GettersTree<S>,
+  Id extends string = string,
+  S extends StateTree = StateTree,
+  G extends GettersTree<S> = GettersTree<S>,
   // has the actions without the context (this) for typings
-  A
+  A = ActionsTree
 > = StoreWithState<Id, S> &
   UnwrapRef<S> &
   StoreWithGetters<G> &
@@ -324,6 +391,11 @@ export type GettersTree<S extends StateTree> = Record<
   ((state: UnwrapRef<S>) => any) | (() => any)
 >
 
+/**
+ * @internal
+ */
+export type ActionsTree = Record<string, _Method>
+
 /**
  * Options parameter of `defineStore()`. Can be extended to augment stores with
  * the plugin API.