]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat!(devtools): group patch changes
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 7 May 2021 15:42:01 +0000 (17:42 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 11 May 2021 20:58:17 +0000 (22:58 +0200)
mutation.type in $subscribe is now an enum

docs/.vitepress/components/ThemeToggle.vue
src/devtools.ts
src/store.ts
src/types.ts

index d192b3ebe23b93d8e6a886dc0c4bfa7c07a10e6e..9ae0debada60dc4fb3efcf6952750c256f7d5f7e 100644 (file)
@@ -58,6 +58,7 @@
   <p>Counter :{{ counterStore.n }}</p>
 
   <button @click="counterStore.increment">Increment</button>
+  <button @click="counterStore.decrementToZero(300, true)">To ZERO</button>
 </template>
 
 <script setup lang="ts">
index e49401ccaa955a6d42f1b7ab59c8fccca332e986..d6846508f55bc1dff5b65b0a1b3f34f06573a923 100644 (file)
@@ -2,10 +2,17 @@ import {
   CustomInspectorNode,
   CustomInspectorState,
   setupDevtoolsPlugin,
+  TimelineEvent,
 } from '@vue/devtools-api'
-import { App } from 'vue'
+import { App, DebuggerEvent } from 'vue'
 import { PiniaPluginContext } from './rootStore'
-import { GenericStore, GettersTree, StateTree } from './types'
+import {
+  GenericStore,
+  GettersTree,
+  MutationType,
+  StateTree,
+  _Method,
+} from './types'
 
 function formatDisplay(display: string) {
   return {
@@ -132,27 +139,41 @@ export function addDevtools(app: App, store: GenericStore) {
         api.sendInspectorState(INSPECTOR_ID)
       }
 
-      store.$subscribe((mutation, state) => {
+      store.$subscribe(({ events, type }, state) => {
         // rootStore.state[store.id] = state
-        const data: Record<string, any> = {
-          store: formatDisplay(mutation.storeName),
-          // type: formatDisplay(mutation.type),
+        console.log('subscribe devtools', events)
+
+        api.notifyComponentUpdate()
+        api.sendInspectorState(INSPECTOR_ID)
+
+        const eventData: TimelineEvent = {
+          time: Date.now(),
+          title: formatMutationType(type),
+          data: formatEventData(events),
         }
 
-        if (mutation.payload) {
-          data.payload = mutation.payload
+        if (type === MutationType.patchFunction) {
+          eventData.subtitle = '⤵️'
+        } else if (type === MutationType.patchObject) {
+          eventData.subtitle = '🧩'
+        } else if (events && !Array.isArray(events)) {
+          eventData.subtitle = events.type
         }
 
-        api.notifyComponentUpdate()
-        api.sendInspectorState(INSPECTOR_ID)
+        if (events) {
+          eventData.data['rawEvent(s)'] = {
+            _custom: {
+              display: 'DebuggerEvent',
+              type: 'object',
+              tooltip: 'raw DebuggerEvent[]',
+              value: events,
+            },
+          }
+        }
 
         api.addTimelineEvent({
           layerId: MUTATIONS_LAYER_ID,
-          event: {
-            time: Date.now(),
-            title: mutation.type,
-            data,
-          },
+          event: eventData,
         })
       })
 
@@ -191,9 +212,79 @@ function formatStoreForInspectorState(
   return fields
 }
 
+function formatEventData(events: DebuggerEvent[] | DebuggerEvent | undefined) {
+  if (!events) return {}
+  if (Array.isArray(events)) {
+    // TODO: handle add and delete for arrays and objects
+    return events.reduce(
+      (data, event) => {
+        data.keys.push(event.key)
+        data.operations.push(event.type)
+        data.oldValue[event.key] = event.oldValue
+        data.newValue[event.key] = event.newValue
+        return data
+      },
+      {
+        oldValue: {} as Record<string, any>,
+        keys: [] as string[],
+        operations: [] as string[],
+        newValue: {} as Record<string, any>,
+      }
+    )
+  } else {
+    return {
+      operation: formatDisplay(events.type),
+      key: formatDisplay(events.key),
+      oldValue: events.oldValue,
+      newValue: events.newValue,
+    }
+  }
+}
+
+function formatMutationType(type: MutationType): string {
+  switch (type) {
+    case MutationType.direct:
+      return 'mutation'
+    case MutationType.patchFunction:
+      return '$patch'
+    case MutationType.patchObject:
+      return '$patch'
+    default:
+      return 'unknown'
+  }
+}
+
 /**
  * pinia.use(devtoolsPlugin)
  */
-export function devtoolsPlugin({ app, store }: PiniaPluginContext) {
+export function devtoolsPlugin<
+  Id extends string = string,
+  S extends StateTree = StateTree,
+  G extends GettersTree<S> = GettersTree<S>,
+  A = Record<string, _Method>
+>({ app, store, options, pinia }: PiniaPluginContext<Id, S, G, A>) {
+  // const wrappedActions: StoreWithActions<A> = {} as StoreWithActions<A>
+  // const actions: A = options.actions || ({} as any)
+
+  // custom patch method
+
+  // for (const actionName in actions) {
+  //   wrappedActions[actionName] = function () {
+  //     setActivePinia(pinia)
+  //     const patchedStore = reactive({
+  //       ...toRefs(store),
+  //       $patch() {
+  //         // TODO: should call subscribe listeners with a group ID
+  //         store.$patch.apply(null, arguments as any)
+  //       },
+  //     })
+  //     // @ts-expect-error: not recognizing it's a _Method for some reason
+  //     return actions[actionName].apply(
+  //       patchedStore,
+  //       (arguments as unknown) as any[]
+  //     )
+  //   } as StoreWithActions<A>[typeof actionName]
+  // }
+
   addDevtools(app, store)
 }
index 01ce2436f9ca5029905b3f4f41131cff4e74a1a2..ecb4fd960f0dc007b73a24468928bea4c5b8a389 100644 (file)
@@ -8,6 +8,8 @@ import {
   onUnmounted,
   InjectionKey,
   provide,
+  DebuggerEvent,
+  WatchOptions,
 } from 'vue'
 import {
   StateTree,
@@ -24,6 +26,7 @@ import {
   StoreDefinition,
   GenericStore,
   GettersTree,
+  MutationType,
 } from './types'
 import {
   getActivePinia,
@@ -103,6 +106,7 @@ function initStore<Id extends string, S extends StateTree>(
 
   let isListening = true
   let subscriptions: SubscriptionCallback<S>[] = []
+  let debuggerEvents: DebuggerEvent[] | DebuggerEvent
 
   function $patch(stateMutation: (state: S) => void): void
   function $patch(partialState: DeepPartial<S>): void
@@ -110,42 +114,72 @@ function initStore<Id extends string, S extends StateTree>(
     partialStateOrMutator: DeepPartial<S> | ((state: S) => void)
   ): void {
     let partialState: DeepPartial<S> = {}
-    let type: string
+    let type: MutationType
     isListening = false
+    // reset the debugger events since patches are sync
+    if (__DEV__) {
+      debuggerEvents = []
+    }
     if (typeof partialStateOrMutator === 'function') {
       partialStateOrMutator(pinia.state.value[$id])
-      type = '🧩 patch'
+      type = MutationType.patchFunction
     } else {
       innerPatch(pinia.state.value[$id], partialStateOrMutator)
       partialState = partialStateOrMutator
-      type = '⤵️ patch'
+      type = MutationType.patchObject
     }
     isListening = true
     // because we paused the watcher, we need to manually call the subscriptions
     subscriptions.forEach((callback) => {
       callback(
-        { storeName: $id, type, payload: partialState },
+        { storeName: $id, type, payload: partialState, events: debuggerEvents },
         pinia.state.value[$id]
       )
     })
   }
 
-  function $subscribe(callback: SubscriptionCallback<S>) {
+  function $subscribe(
+    callback: SubscriptionCallback<S>,
+    onTrigger?: (event: DebuggerEvent) => void
+  ) {
     subscriptions.push(callback)
 
     // watch here to link the subscription to the current active instance
     // e.g. inside the setup of a component
+    const options: WatchOptions = { deep: true, flush: 'sync' }
+    if (__DEV__) {
+      options.onTrigger = (event) => {
+        if (isListening) {
+          debuggerEvents = event
+        } else {
+          // let patch send all the events together later
+          if (Array.isArray(debuggerEvents)) {
+            debuggerEvents.push(event)
+          } else {
+            console.error(
+              '🍍 debuggerEvents should be an array. This is most likely an internal Pinia bug.'
+            )
+          }
+        }
+      }
+    }
     const stopWatcher = watch(
       () => pinia.state.value[$id],
-      (state) => {
+      (state, oldState) => {
         if (isListening) {
-          callback({ storeName: $id, type: '🧩 in place', payload: {} }, state)
+          // TODO: remove payload
+          callback(
+            {
+              storeName: $id,
+              type: MutationType.direct,
+              payload: {},
+              events: debuggerEvents,
+            },
+            state
+          )
         }
       },
-      {
-        deep: true,
-        flush: 'sync',
-      }
+      options
     )
 
     const removeSubscription = () => {
index 0e30b687e06684713bc0e3c91115920442ac3406..3e5a28f04c45764342f4466e5aa4b13a4f3cdff0 100644 (file)
@@ -1,3 +1,4 @@
+import { DebuggerEvent } from 'vue'
 import { Pinia } from './rootStore'
 
 /**
@@ -28,8 +29,53 @@ 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
+}
+
+/**
+ * Callback of a subscription
+ */
 export type SubscriptionCallback<S> = (
-  mutation: { storeName: string; type: string; payload: DeepPartial<S> },
+  // TODO: make type an enumeration
+  // TODO: payload should be optional
+  mutation: {
+    storeName: string
+    type: MutationType
+
+    /**
+     * DEV ONLY. Array for patch calls and single values for direct edits
+     */
+    events?: DebuggerEvent[] | DebuggerEvent
+
+    payload: DeepPartial<S>
+  },
   state: S
 ) => void
 
@@ -90,9 +136,13 @@ export interface StoreWithState<Id extends string, S extends StateTree> {
    * Setups a callback to be called whenever the state changes.
    *
    * @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>): () => void
+  $subscribe(
+    callback: SubscriptionCallback<S>,
+    onTrigger?: (event: DebuggerEvent) => void
+  ): () => void
 }
 
 /**