From c9ce6ea55f225351bb95a47890f791134b233aad Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 11 May 2021 22:21:02 +0200 Subject: [PATCH] feat: subscribe to actions with `$onAction` Implements #240. In alpha, for testing first --- src/index.ts | 2 ++ src/store.ts | 52 ++++++++++++++++++++++++++++-- src/types.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 957c7b68..c65f147b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,8 @@ export type { _Method, StoreWithActions, StoreWithState, + StoreOnActionListener, + StoreOnActionListenerContext, PiniaCustomProperties, DefineStoreOptions, } from './types' diff --git a/src/store.ts b/src/store.ts index 6e6b9db2..5aab347a 100644 --- a/src/store.ts +++ b/src/store.ts @@ -27,6 +27,7 @@ import { GenericStore, GettersTree, MutationType, + StoreOnActionListener, } from './types' import { getActivePinia, @@ -106,6 +107,7 @@ function initStore( let isListening = true let subscriptions: SubscriptionCallback[] = [] + let actionSubscriptions: StoreOnActionListener[] = [] let debuggerEvents: DebuggerEvent[] | DebuggerEvent function $patch(stateMutation: (state: S) => void): void @@ -197,6 +199,23 @@ function initStore( 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( const storeWithState: StoreWithState = { $id, _p: pinia, + _as: actionSubscriptions, // $state is added underneath $patch, $subscribe, + $onAction, $reset, } as StoreWithState @@ -231,6 +252,7 @@ function initStore( ] } +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 = {} as StoreWithActions for (const actionName in actions) { - wrappedActions[actionName] = function () { + wrappedActions[actionName] = function (this: Store) { 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[typeof actionName] } diff --git a/src/types.ts b/src/types.ts index 3e5a28f0..a1c56e02 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 { $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, 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 } /** -- 2.47.2