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 {
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,
})
})
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)
}
onUnmounted,
InjectionKey,
provide,
+ DebuggerEvent,
+ WatchOptions,
} from 'vue'
import {
StateTree,
StoreDefinition,
GenericStore,
GettersTree,
+ MutationType,
} from './types'
import {
getActivePinia,
let isListening = true
let subscriptions: SubscriptionCallback<S>[] = []
+ let debuggerEvents: DebuggerEvent[] | DebuggerEvent
function $patch(stateMutation: (state: S) => void): void
function $patch(partialState: DeepPartial<S>): void
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 = () => {
+import { DebuggerEvent } from 'vue'
import { Pinia } from './rootStore'
/**
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
* 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
}
/**