]> 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>
Tue, 11 May 2021 20:58:17 +0000 (22:58 +0200)
Implements #240. In alpha, for testing first

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

index 957c7b687377191dc25ed81daed2dde6d002abe0..c65f147b664128a53055c7b911e12180f934cbe8 100644 (file)
@@ -13,6 +13,8 @@ export type {
   _Method,
   StoreWithActions,
   StoreWithState,
+  StoreOnActionListener,
+  StoreOnActionListenerContext,
   PiniaCustomProperties,
   DefineStoreOptions,
 } from './types'
index 6e6b9db2a55141937b200e14639dcaa035be6092..5aab347a6017732ff1b5f6e95269e520e15240bf 100644 (file)
@@ -27,6 +27,7 @@ import {
   GenericStore,
   GettersTree,
   MutationType,
+  StoreOnActionListener,
 } from './types'
 import {
   getActivePinia,
@@ -106,6 +107,7 @@ function initStore<Id extends string, S extends StateTree>(
 
   let isListening = true
   let subscriptions: SubscriptionCallback<S>[] = []
+  let actionSubscriptions: StoreOnActionListener[] = []
   let debuggerEvents: DebuggerEvent[] | DebuggerEvent
 
   function $patch(stateMutation: (state: S) => void): void
@@ -197,6 +199,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()
   }
@@ -204,11 +223,13 @@ function initStore<Id extends string, S extends StateTree>(
   const storeWithState: StoreWithState<Id, S> = {
     $id,
     _p: pinia,
+    _as: actionSubscriptions,
 
     // $state is added underneath
 
     $patch,
     $subscribe,
+    $onAction,
     $reset,
   } as StoreWithState<Id, S>
 
@@ -231,6 +252,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
@@ -271,10 +293,34 @@ 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[])
+      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 3e5a28f04c45764342f4466e5aa4b13a4f3cdff0..a1c56e0285edc43882bf754201489c3da5f26419 100644 (file)
@@ -59,6 +59,44 @@ export enum MutationType {
   // 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
  */
@@ -133,16 +171,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)
+   * @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: DebuggerEvent) => 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
+   */
+  $onAction(callback: StoreOnActionListener): () => void
 }
 
 /**