]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat: subscribe to actions with `$onAction`
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 11 May 2021 20:21:02 +0000 (22:21 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 13 May 2021 10:09:36 +0000 (12:09 +0200)
Implements #240. In alpha, for testing first

src/index.ts
src/store.ts
src/types.ts

index a4fb795eb4a016c45eb13b6164febfb232f8de05..b6ccff15de9f5819872402e320ee3446bf16af1b 100644 (file)
@@ -13,6 +13,8 @@ export type {
   _Method,
   StoreWithActions,
   StoreWithState,
+  StoreOnActionListener,
+  StoreOnActionListenerContext,
   PiniaCustomProperties,
   DefineStoreOptions,
 } from './types'
index b73a6a01ffd1256320671ddfdec4ff2169e376b4..e9a7f0ac14f26a73c0cd11b88ae92542772afe40 100644 (file)
@@ -25,6 +25,7 @@ import {
   GettersTree,
   DefineStoreOptions,
   GenericStore,
+  StoreOnActionListener,
 } from './types'
 import { useStoreDevtools } from './devtools'
 import {
@@ -105,6 +106,7 @@ function initStore<Id extends string, S extends StateTree>(
 
   let isListening = true
   const subscriptions: SubscriptionCallback<S>[] = []
+  const actionSubscriptions: StoreOnActionListener[] = []
 
   function $patch(stateMutation: (state: S) => void): void
   function $patch(partialState: DeepPartial<S>): void
@@ -165,6 +167,23 @@ function initStore<Id extends string, S extends StateTree>(
     return removeSubscription
   }
 
+  function $onAction(callback: StoreOnActionListener) {
+    actionSubscriptions.push(callback)
+
+    const removeSubscription = () => {
+      const idx = actionSubscriptions.indexOf(callback)
+      if (idx > -1) {
+        actionSubscriptions.splice(idx, 1)
+      }
+    }
+
+    if (getCurrentInstance()) {
+      onUnmounted(removeSubscription)
+    }
+
+    return removeSubscription
+  }
+
   function $reset() {
     pinia.state.value[$id] = buildState()
   }
@@ -172,11 +191,13 @@ function initStore<Id extends string, S extends StateTree>(
   const storeWithState: StoreWithState<Id, S> = {
     $id,
     _p: markRaw(pinia),
+    _as: actionSubscriptions,
 
     // $state is added underneath
 
     $patch,
     $subscribe,
+    $onAction,
     $reset,
   } as StoreWithState<Id, S>
 
@@ -199,6 +220,7 @@ function initStore<Id extends string, S extends StateTree>(
   ]
 }
 
+const noop = () => {}
 /**
  * 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
@@ -239,10 +261,35 @@ function buildStoreToUse<
 
   const wrappedActions: StoreWithActions<A> = {} as StoreWithActions<A>
   for (const actionName in actions) {
-    wrappedActions[actionName] = function () {
+    wrappedActions[actionName] = function (this: Store<Id, S, G, A>) {
       setActivePinia(pinia)
-      // eslint-disable-next-line
-      return actions[actionName].apply(store, (arguments as unknown) as any[])
+      /* eslint-disable-next-line */
+      const args = Array.from(arguments)
+      const localStore = this || store
+
+      let afterCallback: () => void = noop
+      let onErrorCallback: (error: unknown) => void = noop
+      function after(callback: () => void) {
+        afterCallback = callback
+      }
+      function onError(callback: (error: unknown) => void) {
+        onErrorCallback = callback
+      }
+
+      partialStore._as.forEach((callback) => {
+        callback({ args, name: actionName, store: localStore, after, onError })
+      })
+
+      let ret
+
+      try {
+        ret = actions[actionName].apply(localStore, args as unknown as any[])
+        Promise.resolve(ret).then(afterCallback).catch(onErrorCallback)
+      } catch (error) {
+        throw error
+      }
+
+      return ret
     } as StoreWithActions<A>[typeof actionName]
   }
 
index ca1b1dd0fb9effe950e5eccf95adf79b4a704c82..5099cd644698b430ec98e4787dfcbba4e0a39c41 100644 (file)
@@ -28,6 +28,77 @@ export function isPlainObject(
 export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> }
 // type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }
 
+/**
+ * Possible types for SubscriptionCallback
+ */
+export enum MutationType {
+  /**
+   * Direct mutation of the state:
+   *
+   * - `store.name = 'new name'`
+   * - `store.$state.name = 'new name'`
+   * - `store.list.push('new item')`
+   */
+  direct = 'direct',
+
+  /**
+   * Mutated the state with `$patch` and an object
+   *
+   * - `store.$patch({ name: 'newName' })`
+   */
+  patchObject = 'patch object',
+
+  /**
+   * Mutated the state with `$patch` and a function
+   *
+   * - `store.$patch(state => state.name = 'newName')`
+   */
+  patchFunction = 'patch function',
+
+  // maybe reset? for $state = {} and $reset
+}
+
+/**
+ * Context object passed to callbacks of `store.$onAction(context => {})`
+ */
+export interface StoreOnActionListenerContext {
+  /**
+   * Sets up a hook once the action is finished.
+   */
+  after: (callback: () => void) => void
+
+  /**
+   * Sets up a hook if the action fails.
+   */
+  onError: (callback: (error: unknown) => void) => void
+
+  // TODO: pass generics
+  /**
+   * Store that is invoking the action
+   */
+  store: GenericStore
+
+  /**
+   * Name of the action
+   */
+  name: string
+
+  /**
+   * Parameters passed to the action
+   */
+  args: any[]
+}
+
+/**
+ * Argument of `store.$onAction()`
+ */
+export type StoreOnActionListener = (
+  context: StoreOnActionListenerContext
+) => void
+
+/**
+ * Callback of a subscription
+ */
 export type SubscriptionCallback<S> = (
   mutation: { storeName: string; type: string; payload: DeepPartial<S> },
   state: S
@@ -79,12 +150,63 @@ export interface StoreWithState<Id extends string, S extends StateTree> {
   $reset(): void
 
   /**
-   * Setups a callback to be called whenever the state changes.
+   * Setups a callback to be called whenever the state changes. It also returns
+   * a function to remove the callback. Note than when calling
+   * `store.$subscribe()` inside of a component, it will be automatically
+   * 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
+
+  /**
+   * Array of registered action subscriptions.
+   *
+   * @internal
+   */
+  _as: StoreOnActionListener[]
+
+  /**
+   * @alpha Please send feedback at https://github.com/posva/pinia/issues/240
+   * Setups a callback to be called every time an action is about to get
+   * invoked. The callback receives an object with all the relevant information
+   * of the invoked action:
+   * - `store`: the store it is invoked on
+   * - `name`: The name of the action
+   * - `args`: The parameters passed to the action
+   *
+   * On top of these, it receives two functions that allow setting up a callback
+   * once the action finishes or when it fails.
+   *
+   * It also returns a function to remove the callback. Note than when calling
+   * `store.$onAction()` inside of a component, it will be automatically cleanup
+   * up when the component gets unmounted.
+   *
+   * @example
+   *
+   *```js
+   *store.$onAction(({ after, onError }) => {
+   *  // Here you could share variables between all of the hooks as well as
+   *  // setting up watchers and clean them up
+   *  after(() => {
+   *    // can be used to cleanup side effects
+   *  })
+   *  onError((error) => {
+   *    // can be used to pass up errors
+   *  })
+   *})
+   *```
+   *
+   * @param callback - callback called before every action
    * @returns function that removes the watcher
    */
-  $subscribe(callback: SubscriptionCallback<S>): () => void
+  $onAction(callback: StoreOnActionListener): () => void
 }
 
 /**