From 79c488ca8b9d185eb3911e07c2d362f068989877 Mon Sep 17 00:00:00 2001 From: KrosFire Date: Wed, 24 Aug 2022 00:08:01 +0200 Subject: [PATCH 1/6] Added functionality for checking differences in state --- .../pinia/__tests__/devtools/utils.spec.ts | 277 ++++++++++++++++++ packages/pinia/package.json | 2 +- packages/pinia/src/devtools/plugin.ts | 10 +- packages/pinia/src/devtools/utils.ts | 77 +++++ 4 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 packages/pinia/__tests__/devtools/utils.spec.ts diff --git a/packages/pinia/__tests__/devtools/utils.spec.ts b/packages/pinia/__tests__/devtools/utils.spec.ts new file mode 100644 index 0000000000..c105466e18 --- /dev/null +++ b/packages/pinia/__tests__/devtools/utils.spec.ts @@ -0,0 +1,277 @@ +import { describe, it, expect } from 'vitest' +import { formatStateDifferences, realTypeOf } from '../../src/devtools/utils' + +describe('Devtools utils', () => { + + describe('realTypeOf', () => { + it('Should correctly predict type of subject', () => { + const number = 0 + const string = 'undefined' + const undefinedValue = undefined + const nullValue = null + const array: any[] = [] + const date = new Date(123) + const object = {} + const regexp = /regexp/ + const functionValue = () => {} + + let type = realTypeOf(number) + + expect(type).toEqual('number') + + type = realTypeOf(string) + + expect(type).toEqual('string') + + type = realTypeOf(undefinedValue) + + expect(type).toEqual('undefined') + + type = realTypeOf(nullValue) + + expect(type).toEqual('null') + + type = realTypeOf(array) + + expect(type).toEqual('array') + + type = realTypeOf(date) + + expect(type).toEqual('date') + + type = realTypeOf(object) + + expect(type).toEqual('object') + + type = realTypeOf(regexp) + + expect(type).toEqual('regexp') + + type = realTypeOf(functionValue) + + expect(type).toEqual('function') + }) + }) + + describe('formatStateDifferences', () => { + it('Should find removed entries', () => { + const oldState = { + removed: 'old' + } + const newState = {} + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + removed: undefined, + }) + }) + + it('Should find difference in array', () => { + const oldState = { + changedArray1: [1, 2, 3], + unchangedArray: [1, 2, 3], + changedArray2: [1, 2, 3] + } + const newState = { + changedArray1: [1, 2, 3, 4], + unchangedArray: [1, 2, 3], + changedArray2: [3, 2, 1] + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + changedArray1: [1, 2, 3, 4], + changedArray2: [3, 2, 1] + }) + }) + + it('Should find difference in regexp', () => { + const oldState = { + changedRegexp: /changed/, + unchangedRegexp: /unchanged/ + } + const newState = { + changedRegexp: /changedToNewValue/, + unchangedRegexp: /unchanged/ + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + changedRegexp: /changedToNewValue/, + }) + }) + + it('Should find difference in date', () => { + const oldState = { + changedDate: new Date(123), + unchangedDate: new Date(123) + } + const newState = { + changedDate: new Date(1234), + unchangedDate: new Date(123) + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + changedDate: new Date(1234), + }) + }) + + it('Should find difference in booleans', () => { + const oldState = { + changedBool: true, + unchangedBool: true + } + const newState = { + changedBool: false, + unchangedBool: true + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + changedBool: false, + }) + }) + + it('Should find difference in numbers', () => { + const oldState = { + changedNumber: 10, + unchangedNumber: 10 + } + const newState = { + changedNumber: 9, + unchangedNumber: 10 + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + changedNumber: 9, + }) + }) + + it('Should find difference in strings', () => { + const oldState = { + changedString: 'changed', + unchangedString: 'unchanged' + } + const newState = { + changedString: 'changedToNewValue', + unchangedString: 'unchanged' + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + changedString: 'changedToNewValue', + }) + }) + + it('Should find new values', () => { + const oldState = { + } + const newState = { + newValue: 10 + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + newValue: 10 + }) + }) + + it('Should correctly see changes deep in objects', () => { + const oldState = { + changedObject: { + key1: 'unchanged', + key2: { + key1: { + key1: { + key1: false, + key2: true + } + } + }, + key3: { + key1: { + key1: {} + }, + key2: { + key1: 'abc' + } + }, + key4: 50 + } + } + const newState = { + changedObject: { + key1: 'unchanged', + key2: { + key1: { + key1: { + key1: true, + key2: true + } + } + }, + key3: { + key1: { + key1: {} + }, + key2: { + key1: 'abcd' + } + }, + key4: 50 + } + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + changedObject: { + key2: { + key1: { + key1: { + key1: true, + } + } + }, + key3: { + key2: { + key1: 'abcd' + } + }, + } + }) + }) + + it('Should find the difference between functions', () => { + const foo = () => {} + const bar = () => {} + const foobar = () => {} + + const oldState = { + foo, + bar + } + + const newState = { + foo: foobar, + bar + } + + const differences = formatStateDifferences(oldState, newState) + + expect(differences).toEqual({ + foo: foobar + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/pinia/package.json b/packages/pinia/package.json index fe30a0ae8f..69be01145e 100644 --- a/packages/pinia/package.json +++ b/packages/pinia/package.json @@ -1,6 +1,6 @@ { "name": "pinia", - "version": "2.0.20", + "version": "2.0.21", "description": "Intuitive, type safe and flexible Store for Vue", "main": "index.js", "module": "dist/pinia.mjs", diff --git a/packages/pinia/src/devtools/plugin.ts b/packages/pinia/src/devtools/plugin.ts index d62ac5f0d8..bb2ad19ab0 100644 --- a/packages/pinia/src/devtools/plugin.ts +++ b/packages/pinia/src/devtools/plugin.ts @@ -27,7 +27,7 @@ import { PINIA_ROOT_ID, PINIA_ROOT_LABEL, } from './formatting' -import { isPinia, toastMessage } from './utils' +import { isPinia, toastMessage, formatStateDifferences } from './utils' // timeline can be paused when directly changing the state let isTimelineActive = true @@ -328,6 +328,8 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { store.$onAction(({ after, onError, name, args }) => { const groupId = runningActionId++ + const initialState = JSON.parse(JSON.stringify(toRaw(store.$state))); + api.addTimelineEvent({ layerId: MUTATIONS_LAYER_ID, event: { @@ -337,6 +339,7 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { data: { store: formatDisplay(store.$id), action: formatDisplay(name), + initialState, args, }, groupId, @@ -345,6 +348,9 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { after((result) => { activeAction = undefined + const newState = toRaw(store.$state) + const differences = formatStateDifferences(initialState, newState) + api.addTimelineEvent({ layerId: MUTATIONS_LAYER_ID, event: { @@ -355,6 +361,8 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { store: formatDisplay(store.$id), action: formatDisplay(name), args, + newState, + differences, result, }, groupId, diff --git a/packages/pinia/src/devtools/utils.ts b/packages/pinia/src/devtools/utils.ts index 07718b668c..d6849a0f10 100644 --- a/packages/pinia/src/devtools/utils.ts +++ b/packages/pinia/src/devtools/utils.ts @@ -1,4 +1,5 @@ import { Pinia } from '../rootStore' +import { StateTree } from '../types' /** * Shows a toast or console.log @@ -26,3 +27,79 @@ export function toastMessage( export function isPinia(o: any): o is Pinia { return '_a' in o && 'install' in o } + +export const realTypeOf = (subject: any) => { + const type = typeof subject; + if (type !== 'object') return type; + + if (subject === Math) { + return 'math'; + } else if (subject === null) { + return 'null'; + } else if (Array.isArray(subject)) { + return 'array'; + } else if (Object.prototype.toString.call(subject) === '[object Date]') { + return 'date'; + } else if (typeof subject.toString === 'function' && /^\/.*\//.test(subject.toString())) { + return 'regexp'; + } + return 'object'; +} + +export function formatStateDifferences( + initialState: StateTree, + newState: StateTree, +): StateTree { + const stateDifferences: StateTree = {} + + for (const key in newState) { + const oldType = realTypeOf(initialState[key]) + const newType = realTypeOf(newState[key]) + + if (oldType !== newType) { + stateDifferences[key] = newState[key] + break + } + + switch (newType) { + case 'object': + const oldHash = JSON.stringify(initialState[key]) + const newHash = JSON.stringify(newState[key]) + + if (oldHash !== newHash) { + const diffsInObject = formatStateDifferences( + initialState[key], + newState[key] + ) + + if (Object.keys(diffsInObject).length) { + stateDifferences[key] = diffsInObject + } + } + break + case 'date': + if (initialState[key] - newState[key] !== 0) { + stateDifferences[key] = newState[key] + } + break + case 'array': + case 'regexp': + if (initialState[key].toString() !== newState[key].toString()) { + stateDifferences[key] = newState[key] + } + break; + default: + if (initialState[key] !== newState[key]) { + stateDifferences[key] = newState[key] + } + } + } + + Object.keys(initialState).forEach(key => { + if (!(key in newState)) { + stateDifferences[key] = undefined + } + }) + + return stateDifferences +} \ No newline at end of file From 2fdcec1e1afcdd29c1341ce26855d2204474e5e9 Mon Sep 17 00:00:00 2001 From: KrosFire Date: Wed, 24 Aug 2022 00:12:00 +0200 Subject: [PATCH 2/6] Reverted version bump --- packages/pinia/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pinia/package.json b/packages/pinia/package.json index 69be01145e..fe30a0ae8f 100644 --- a/packages/pinia/package.json +++ b/packages/pinia/package.json @@ -1,6 +1,6 @@ { "name": "pinia", - "version": "2.0.21", + "version": "2.0.20", "description": "Intuitive, type safe and flexible Store for Vue", "main": "index.js", "module": "dist/pinia.mjs", From 8d6cd8e03eae73f5c6f148e34778c4a3ad7bff04 Mon Sep 17 00:00:00 2001 From: KrosFire Date: Wed, 24 Aug 2022 00:31:30 +0200 Subject: [PATCH 3/6] Hotfixes --- packages/pinia/src/devtools/plugin.ts | 4 ++-- packages/pinia/src/devtools/utils.ts | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/pinia/src/devtools/plugin.ts b/packages/pinia/src/devtools/plugin.ts index bb2ad19ab0..b8162c985b 100644 --- a/packages/pinia/src/devtools/plugin.ts +++ b/packages/pinia/src/devtools/plugin.ts @@ -27,7 +27,7 @@ import { PINIA_ROOT_ID, PINIA_ROOT_LABEL, } from './formatting' -import { isPinia, toastMessage, formatStateDifferences } from './utils' +import { isPinia, toastMessage, formatStateDifferences, deepCopy } from './utils' // timeline can be paused when directly changing the state let isTimelineActive = true @@ -328,7 +328,7 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { store.$onAction(({ after, onError, name, args }) => { const groupId = runningActionId++ - const initialState = JSON.parse(JSON.stringify(toRaw(store.$state))); + const initialState = deepCopy(toRaw(store.$state)); api.addTimelineEvent({ layerId: MUTATIONS_LAYER_ID, diff --git a/packages/pinia/src/devtools/utils.ts b/packages/pinia/src/devtools/utils.ts index d6849a0f10..5bdf891217 100644 --- a/packages/pinia/src/devtools/utils.ts +++ b/packages/pinia/src/devtools/utils.ts @@ -46,6 +46,19 @@ export const realTypeOf = (subject: any) => { return 'object'; } +export function deepCopy(object: R) { + const objectCopy: R = {} as R + for (const key in object) { + if (realTypeOf(object[key]) === 'object') { + objectCopy[key] = deepCopy(object[key]) + } else { + objectCopy[key] = object[key] + } + } + + return objectCopy +} + export function formatStateDifferences( initialState: StateTree, newState: StateTree, @@ -58,7 +71,7 @@ export function formatStateDifferences( if (oldType !== newType) { stateDifferences[key] = newState[key] - break + continue } switch (newType) { From cd361eae2324e38c1e5951ee73f324d01f240f71 Mon Sep 17 00:00:00 2001 From: KrosFire Date: Wed, 24 Aug 2022 00:40:09 +0200 Subject: [PATCH 4/6] Removed deepCopy --- packages/pinia/src/devtools/plugin.ts | 4 ++-- packages/pinia/src/devtools/utils.ts | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/pinia/src/devtools/plugin.ts b/packages/pinia/src/devtools/plugin.ts index b8162c985b..bb2ad19ab0 100644 --- a/packages/pinia/src/devtools/plugin.ts +++ b/packages/pinia/src/devtools/plugin.ts @@ -27,7 +27,7 @@ import { PINIA_ROOT_ID, PINIA_ROOT_LABEL, } from './formatting' -import { isPinia, toastMessage, formatStateDifferences, deepCopy } from './utils' +import { isPinia, toastMessage, formatStateDifferences } from './utils' // timeline can be paused when directly changing the state let isTimelineActive = true @@ -328,7 +328,7 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { store.$onAction(({ after, onError, name, args }) => { const groupId = runningActionId++ - const initialState = deepCopy(toRaw(store.$state)); + const initialState = JSON.parse(JSON.stringify(toRaw(store.$state))); api.addTimelineEvent({ layerId: MUTATIONS_LAYER_ID, diff --git a/packages/pinia/src/devtools/utils.ts b/packages/pinia/src/devtools/utils.ts index 5bdf891217..b9dc5262bd 100644 --- a/packages/pinia/src/devtools/utils.ts +++ b/packages/pinia/src/devtools/utils.ts @@ -46,19 +46,6 @@ export const realTypeOf = (subject: any) => { return 'object'; } -export function deepCopy(object: R) { - const objectCopy: R = {} as R - for (const key in object) { - if (realTypeOf(object[key]) === 'object') { - objectCopy[key] = deepCopy(object[key]) - } else { - objectCopy[key] = object[key] - } - } - - return objectCopy -} - export function formatStateDifferences( initialState: StateTree, newState: StateTree, From 43b6bbc454bb914ad647885ccf30e5b3c144ab06 Mon Sep 17 00:00:00 2001 From: KrosFire Date: Wed, 24 Aug 2022 21:26:13 +0200 Subject: [PATCH 5/6] Implemented fast copy --- packages/pinia/src/devtools/fastCopy.ts | 408 ++++++++++++++++++++++++ packages/pinia/src/devtools/plugin.ts | 5 +- 2 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 packages/pinia/src/devtools/fastCopy.ts diff --git a/packages/pinia/src/devtools/fastCopy.ts b/packages/pinia/src/devtools/fastCopy.ts new file mode 100644 index 0000000000..a3df14e3a0 --- /dev/null +++ b/packages/pinia/src/devtools/fastCopy.ts @@ -0,0 +1,408 @@ +// Clone deep utility for cloning state of the store +// Forked from https://github.com/planttheidea/fast-copy +// Last update: 24-08-2022 + +declare namespace FastCopy { + export type Realm = Record; + + export interface Cache { + _keys?: any[]; + _values?: any[]; + has: (value: any) => boolean; + set: (key: any, value: any) => void; + get: (key: any) => any; + } + + export type Copier = (value: Value, cache: Cache) => Value; + + export type ObjectCloner = ( + object: Value, + realm: Realm, + handleCopy: Copier, + cache: Cache, + ) => Value; + + export type Options = { + isStrict?: boolean; + realm?: Realm; + }; +} + +const { toString: toStringFunction } = Function.prototype; +const { + create, + defineProperty, + getOwnPropertyDescriptor, + getOwnPropertyNames, + getOwnPropertySymbols, + getPrototypeOf, +} = Object; + +const SYMBOL_PROPERTIES = typeof getOwnPropertySymbols === 'function'; +const WEAK_MAP = typeof WeakMap === 'function'; + +/** + * @function createCache + * + * @description + * get a new cache object to prevent circular references + * + * @returns the new cache object + */ +export const createCache = (() => { + if (WEAK_MAP) { + return (): FastCopy.Cache => new WeakMap(); + } + + class Cache { + _keys: any[] = []; + _values: any[] = []; + + has(key: any) { + return !!~this._keys.indexOf(key); + } + + get(key: any) { + return this._values[this._keys.indexOf(key)]; + } + + set(key: any, value: any) { + this._keys.push(key); + this._values.push(value); + } + } + + return (): FastCopy.Cache => new Cache(); +})(); + +/** + * @function getCleanClone + * + * @description + * get an empty version of the object with the same prototype it has + * + * @param object the object to build a clean clone from + * @param realm the realm the object resides in + * @returns the empty cloned object + */ +export const getCleanClone = (object: any, realm: FastCopy.Realm): any => { + const prototype = object.__proto__ || getPrototypeOf(object); + + if (!prototype) { + return create(null); + } + + const Constructor = prototype.constructor; + + if (Constructor === realm.Object) { + return prototype === realm.Object.prototype ? {} : create(prototype); + } + + if (~toStringFunction.call(Constructor).indexOf('[native code]')) { + try { + return new Constructor(); + } catch {} + } + + return create(prototype); +}; + +/** + * @function getObjectCloneStrict + * + * @description + * get a copy of the object based on strict rules, meaning all keys and symbols + * are copied based on the original property descriptors + * + * @param object the object to clone + * @param realm the realm the object resides in + * @param handleCopy the function that handles copying the object + * @returns the copied object + */ +export const getObjectCloneStrict: FastCopy.ObjectCloner = ( + object: any, + realm: FastCopy.Realm, + handleCopy: FastCopy.Copier, + cache: FastCopy.Cache, +): any => { + const clone: any = getCleanClone(object, realm); + + // set in the cache immediately to be able to reuse the object recursively + cache.set(object, clone); + + const properties: (string | symbol)[] = SYMBOL_PROPERTIES + ? getOwnPropertyNames(object).concat( + getOwnPropertySymbols(object) as unknown as string[], + ) + : getOwnPropertyNames(object); + + for ( + let index = 0, length = properties.length, property, descriptor; + index < length; + ++index + ) { + property = properties[index]; + + if (property !== 'callee' && property !== 'caller') { + descriptor = getOwnPropertyDescriptor(object, property); + + if (descriptor) { + // Only clone the value if actually a value, not a getter / setter. + if (!descriptor.get && !descriptor.set) { + descriptor.value = handleCopy(object[property], cache); + } + + try { + defineProperty(clone, property, descriptor); + } catch (error) { + // Tee above can fail on node in edge cases, so fall back to the loose assignment. + clone[property] = descriptor.value; + } + } else { + // In extra edge cases where the property descriptor cannot be retrived, fall back to + // the loose assignment. + clone[property] = handleCopy(object[property], cache); + } + } + } + + return clone; +}; + +/** + * @function getRegExpFlags + * + * @description + * get the flags to apply to the copied regexp + * + * @param regExp the regexp to get the flags of + * @returns the flags for the regexp + */ +export const getRegExpFlags = (regExp: RegExp): string => { + let flags = ''; + + if (regExp.global) { + flags += 'g'; + } + + if (regExp.ignoreCase) { + flags += 'i'; + } + + if (regExp.multiline) { + flags += 'm'; + } + + if (regExp.unicode) { + flags += 'u'; + } + + if (regExp.sticky) { + flags += 'y'; + } + + return flags; +}; + +const { isArray } = Array; + +const GLOBAL_THIS: FastCopy.Realm = (function () { + if (typeof globalThis !== 'undefined') { + return globalThis; + } + + if (typeof self !== 'undefined') { + return self; + } + + if (typeof window !== 'undefined') { + return window; + } + + if (typeof global !== 'undefined') { + return global; + } + + if (console && console.error) { + console.error('Unable to locate global object, returning "this".'); + } + + // @ts-ignore + return this; +})(); + +/** + * @function copy + * + * @description + * copy an value deeply as much as possible + * + * If `strict` is applied, then all properties (including non-enumerable ones) + * are copied with their original property descriptors on both objects and arrays. + * + * The value is compared to the global constructors in the `realm` provided, + * and the native constructor is always used to ensure that extensions of native + * objects (allows in ES2015+) are maintained. + * + * @param value the value to copy + * @param [options] the options for copying with + * @param [options.isStrict] should the copy be strict + * @param [options.realm] the realm (this) value the value is copied from + * @returns the copied value + */ +function copy(value: Value, options?: FastCopy.Options): Value { + // manually coalesced instead of default parameters for performance + const realm = (options && options.realm) || GLOBAL_THIS; + const getObjectClone = getObjectCloneStrict; + + /** + * @function handleCopy + * + * @description + * copy the value recursively based on its type + * + * @param value the value to copy + * @returns the copied value + */ + const handleCopy: FastCopy.Copier = ( + value: any, + cache: FastCopy.Cache, + ): any => { + if (!value || typeof value !== 'object') { + return value; + } + + if (cache.has(value)) { + return cache.get(value); + } + + const prototype = value.__proto__ || getPrototypeOf(value); + const Constructor = prototype && prototype.constructor; + + // plain objects + if (!Constructor || Constructor === realm.Object) { + return getObjectClone(value, realm, handleCopy, cache); + } + + let clone: any; + + // arrays + if (isArray(value)) { + return getObjectCloneStrict(value, realm, handleCopy, cache); + } + + // dates + if (value instanceof realm.Date) { + return new Constructor(value.getTime()); + } + + // regexps + if (value instanceof realm.RegExp) { + clone = new Constructor( + value.source, + value.flags || getRegExpFlags(value), + ); + + clone.lastIndex = value.lastIndex; + + return clone; + } + + // maps + if (realm.Map && value instanceof realm.Map) { + clone = new Constructor(); + cache.set(value, clone); + + value.forEach((value: any, key: any) => { + clone.set(key, handleCopy(value, cache)); + }); + + return clone; + } + + // sets + if (realm.Set && value instanceof realm.Set) { + clone = new Constructor(); + cache.set(value, clone); + + value.forEach((value: any) => { + clone.add(handleCopy(value, cache)); + }); + + return clone; + } + + // blobs + if (realm.Blob && value instanceof realm.Blob) { + return value.slice(0, value.size, value.type); + } + + // buffers (node-only) + if (realm.Buffer && realm.Buffer.isBuffer(value)) { + clone = realm.Buffer.allocUnsafe + ? realm.Buffer.allocUnsafe(value.length) + : new Constructor(value.length); + + cache.set(value, clone); + value.copy(clone); + + return clone; + } + + // arraybuffers / dataviews + if (realm.ArrayBuffer) { + // dataviews + if (realm.ArrayBuffer.isView(value)) { + clone = new Constructor(value.buffer.slice(0)); + cache.set(value, clone); + return clone; + } + + // arraybuffers + if (value instanceof realm.ArrayBuffer) { + clone = value.slice(0); + cache.set(value, clone); + return clone; + } + } + + // if the value cannot / should not be cloned, don't + if ( + // promise-like + typeof value.then === 'function' || + // errors + value instanceof Error || + // weakmaps + (realm.WeakMap && value instanceof realm.WeakMap) || + // weaksets + (realm.WeakSet && value instanceof realm.WeakSet) + ) { + return value; + } + + // assume anything left is a custom constructor + return getObjectClone(value, realm, handleCopy, cache); + }; + + return handleCopy(value, createCache()); +} + +/** + * @function strictCopy + * + * @description + * copy the value with `strict` option pre-applied + * + * @param value the value to copy + * @param [options] the options for copying with + * @param [options.realm] the realm (this) value the value is copied from + * @returns the copied value + */ +copy.strict = function strictCopy(value: any, options?: FastCopy.Options) { + return copy(value, { + isStrict: true, + realm: options ? options.realm : void 0, + }); +}; + +export default copy; \ No newline at end of file diff --git a/packages/pinia/src/devtools/plugin.ts b/packages/pinia/src/devtools/plugin.ts index bb2ad19ab0..3da1619902 100644 --- a/packages/pinia/src/devtools/plugin.ts +++ b/packages/pinia/src/devtools/plugin.ts @@ -28,6 +28,7 @@ import { PINIA_ROOT_LABEL, } from './formatting' import { isPinia, toastMessage, formatStateDifferences } from './utils' +import copy from './fastCopy' // timeline can be paused when directly changing the state let isTimelineActive = true @@ -328,7 +329,7 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { store.$onAction(({ after, onError, name, args }) => { const groupId = runningActionId++ - const initialState = JSON.parse(JSON.stringify(toRaw(store.$state))); + const initialState = copy(store.$state); api.addTimelineEvent({ layerId: MUTATIONS_LAYER_ID, @@ -348,7 +349,7 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { after((result) => { activeAction = undefined - const newState = toRaw(store.$state) + const newState = store.$state const differences = formatStateDifferences(initialState, newState) api.addTimelineEvent({ From c19379d554dae288a58fbbafacd6f2bc48208990 Mon Sep 17 00:00:00 2001 From: KrosFire Date: Wed, 24 Aug 2022 21:26:57 +0200 Subject: [PATCH 6/6] Fixed formatting --- .../pinia/__tests__/devtools/utils.spec.ts | 86 ++++---- packages/pinia/src/devtools/fastCopy.ts | 208 +++++++++--------- packages/pinia/src/devtools/plugin.ts | 2 +- packages/pinia/src/devtools/utils.ts | 29 +-- 4 files changed, 163 insertions(+), 162 deletions(-) diff --git a/packages/pinia/__tests__/devtools/utils.spec.ts b/packages/pinia/__tests__/devtools/utils.spec.ts index c105466e18..cd1c3e787e 100644 --- a/packages/pinia/__tests__/devtools/utils.spec.ts +++ b/packages/pinia/__tests__/devtools/utils.spec.ts @@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest' import { formatStateDifferences, realTypeOf } from '../../src/devtools/utils' describe('Devtools utils', () => { - describe('realTypeOf', () => { it('Should correctly predict type of subject', () => { const number = 0 @@ -56,7 +55,7 @@ describe('Devtools utils', () => { describe('formatStateDifferences', () => { it('Should find removed entries', () => { const oldState = { - removed: 'old' + removed: 'old', } const newState = {} @@ -71,30 +70,30 @@ describe('Devtools utils', () => { const oldState = { changedArray1: [1, 2, 3], unchangedArray: [1, 2, 3], - changedArray2: [1, 2, 3] + changedArray2: [1, 2, 3], } const newState = { changedArray1: [1, 2, 3, 4], unchangedArray: [1, 2, 3], - changedArray2: [3, 2, 1] + changedArray2: [3, 2, 1], } const differences = formatStateDifferences(oldState, newState) expect(differences).toEqual({ changedArray1: [1, 2, 3, 4], - changedArray2: [3, 2, 1] + changedArray2: [3, 2, 1], }) }) it('Should find difference in regexp', () => { const oldState = { changedRegexp: /changed/, - unchangedRegexp: /unchanged/ + unchangedRegexp: /unchanged/, } const newState = { changedRegexp: /changedToNewValue/, - unchangedRegexp: /unchanged/ + unchangedRegexp: /unchanged/, } const differences = formatStateDifferences(oldState, newState) @@ -107,11 +106,11 @@ describe('Devtools utils', () => { it('Should find difference in date', () => { const oldState = { changedDate: new Date(123), - unchangedDate: new Date(123) + unchangedDate: new Date(123), } const newState = { changedDate: new Date(1234), - unchangedDate: new Date(123) + unchangedDate: new Date(123), } const differences = formatStateDifferences(oldState, newState) @@ -124,11 +123,11 @@ describe('Devtools utils', () => { it('Should find difference in booleans', () => { const oldState = { changedBool: true, - unchangedBool: true + unchangedBool: true, } const newState = { changedBool: false, - unchangedBool: true + unchangedBool: true, } const differences = formatStateDifferences(oldState, newState) @@ -141,11 +140,11 @@ describe('Devtools utils', () => { it('Should find difference in numbers', () => { const oldState = { changedNumber: 10, - unchangedNumber: 10 + unchangedNumber: 10, } const newState = { changedNumber: 9, - unchangedNumber: 10 + unchangedNumber: 10, } const differences = formatStateDifferences(oldState, newState) @@ -158,11 +157,11 @@ describe('Devtools utils', () => { it('Should find difference in strings', () => { const oldState = { changedString: 'changed', - unchangedString: 'unchanged' + unchangedString: 'unchanged', } const newState = { changedString: 'changedToNewValue', - unchangedString: 'unchanged' + unchangedString: 'unchanged', } const differences = formatStateDifferences(oldState, newState) @@ -173,16 +172,15 @@ describe('Devtools utils', () => { }) it('Should find new values', () => { - const oldState = { - } + const oldState = {} const newState = { - newValue: 10 + newValue: 10, } const differences = formatStateDifferences(oldState, newState) expect(differences).toEqual({ - newValue: 10 + newValue: 10, }) }) @@ -194,20 +192,20 @@ describe('Devtools utils', () => { key1: { key1: { key1: false, - key2: true - } - } + key2: true, + }, + }, }, key3: { key1: { - key1: {} + key1: {}, }, key2: { - key1: 'abc' - } + key1: 'abc', + }, }, - key4: 50 - } + key4: 50, + }, } const newState = { changedObject: { @@ -216,20 +214,20 @@ describe('Devtools utils', () => { key1: { key1: { key1: true, - key2: true - } - } + key2: true, + }, + }, }, key3: { key1: { - key1: {} + key1: {}, }, key2: { - key1: 'abcd' - } + key1: 'abcd', + }, }, - key4: 50 - } + key4: 50, + }, } const differences = formatStateDifferences(oldState, newState) @@ -240,15 +238,15 @@ describe('Devtools utils', () => { key1: { key1: { key1: true, - } - } + }, + }, }, key3: { key2: { - key1: 'abcd' - } + key1: 'abcd', + }, }, - } + }, }) }) @@ -259,19 +257,19 @@ describe('Devtools utils', () => { const oldState = { foo, - bar + bar, } const newState = { foo: foobar, - bar + bar, } const differences = formatStateDifferences(oldState, newState) expect(differences).toEqual({ - foo: foobar + foo: foobar, }) }) }) -}) \ No newline at end of file +}) diff --git a/packages/pinia/src/devtools/fastCopy.ts b/packages/pinia/src/devtools/fastCopy.ts index a3df14e3a0..780f7a241b 100644 --- a/packages/pinia/src/devtools/fastCopy.ts +++ b/packages/pinia/src/devtools/fastCopy.ts @@ -3,32 +3,32 @@ // Last update: 24-08-2022 declare namespace FastCopy { - export type Realm = Record; + export type Realm = Record export interface Cache { - _keys?: any[]; - _values?: any[]; - has: (value: any) => boolean; - set: (key: any, value: any) => void; - get: (key: any) => any; + _keys?: any[] + _values?: any[] + has: (value: any) => boolean + set: (key: any, value: any) => void + get: (key: any) => any } - export type Copier = (value: Value, cache: Cache) => Value; + export type Copier = (value: Value, cache: Cache) => Value export type ObjectCloner = ( object: Value, realm: Realm, handleCopy: Copier, - cache: Cache, - ) => Value; + cache: Cache + ) => Value export type Options = { - isStrict?: boolean; - realm?: Realm; - }; + isStrict?: boolean + realm?: Realm + } } -const { toString: toStringFunction } = Function.prototype; +const { toString: toStringFunction } = Function.prototype const { create, defineProperty, @@ -36,10 +36,10 @@ const { getOwnPropertyNames, getOwnPropertySymbols, getPrototypeOf, -} = Object; +} = Object -const SYMBOL_PROPERTIES = typeof getOwnPropertySymbols === 'function'; -const WEAK_MAP = typeof WeakMap === 'function'; +const SYMBOL_PROPERTIES = typeof getOwnPropertySymbols === 'function' +const WEAK_MAP = typeof WeakMap === 'function' /** * @function createCache @@ -51,29 +51,29 @@ const WEAK_MAP = typeof WeakMap === 'function'; */ export const createCache = (() => { if (WEAK_MAP) { - return (): FastCopy.Cache => new WeakMap(); + return (): FastCopy.Cache => new WeakMap() } class Cache { - _keys: any[] = []; - _values: any[] = []; + _keys: any[] = [] + _values: any[] = [] has(key: any) { - return !!~this._keys.indexOf(key); + return !!~this._keys.indexOf(key) } get(key: any) { - return this._values[this._keys.indexOf(key)]; + return this._values[this._keys.indexOf(key)] } set(key: any, value: any) { - this._keys.push(key); - this._values.push(value); + this._keys.push(key) + this._values.push(value) } } - return (): FastCopy.Cache => new Cache(); -})(); + return (): FastCopy.Cache => new Cache() +})() /** * @function getCleanClone @@ -86,26 +86,26 @@ export const createCache = (() => { * @returns the empty cloned object */ export const getCleanClone = (object: any, realm: FastCopy.Realm): any => { - const prototype = object.__proto__ || getPrototypeOf(object); + const prototype = object.__proto__ || getPrototypeOf(object) if (!prototype) { - return create(null); + return create(null) } - const Constructor = prototype.constructor; + const Constructor = prototype.constructor if (Constructor === realm.Object) { - return prototype === realm.Object.prototype ? {} : create(prototype); + return prototype === realm.Object.prototype ? {} : create(prototype) } if (~toStringFunction.call(Constructor).indexOf('[native code]')) { try { - return new Constructor(); + return new Constructor() } catch {} } - return create(prototype); -}; + return create(prototype) +} /** * @function getObjectCloneStrict @@ -123,51 +123,51 @@ export const getObjectCloneStrict: FastCopy.ObjectCloner = ( object: any, realm: FastCopy.Realm, handleCopy: FastCopy.Copier, - cache: FastCopy.Cache, + cache: FastCopy.Cache ): any => { - const clone: any = getCleanClone(object, realm); + const clone: any = getCleanClone(object, realm) // set in the cache immediately to be able to reuse the object recursively - cache.set(object, clone); + cache.set(object, clone) const properties: (string | symbol)[] = SYMBOL_PROPERTIES ? getOwnPropertyNames(object).concat( - getOwnPropertySymbols(object) as unknown as string[], + getOwnPropertySymbols(object) as unknown as string[] ) - : getOwnPropertyNames(object); + : getOwnPropertyNames(object) for ( let index = 0, length = properties.length, property, descriptor; index < length; ++index ) { - property = properties[index]; + property = properties[index] if (property !== 'callee' && property !== 'caller') { - descriptor = getOwnPropertyDescriptor(object, property); + descriptor = getOwnPropertyDescriptor(object, property) if (descriptor) { // Only clone the value if actually a value, not a getter / setter. if (!descriptor.get && !descriptor.set) { - descriptor.value = handleCopy(object[property], cache); + descriptor.value = handleCopy(object[property], cache) } try { - defineProperty(clone, property, descriptor); + defineProperty(clone, property, descriptor) } catch (error) { // Tee above can fail on node in edge cases, so fall back to the loose assignment. - clone[property] = descriptor.value; + clone[property] = descriptor.value } } else { // In extra edge cases where the property descriptor cannot be retrived, fall back to // the loose assignment. - clone[property] = handleCopy(object[property], cache); + clone[property] = handleCopy(object[property], cache) } } } - return clone; -}; + return clone +} /** * @function getRegExpFlags @@ -179,57 +179,57 @@ export const getObjectCloneStrict: FastCopy.ObjectCloner = ( * @returns the flags for the regexp */ export const getRegExpFlags = (regExp: RegExp): string => { - let flags = ''; + let flags = '' if (regExp.global) { - flags += 'g'; + flags += 'g' } if (regExp.ignoreCase) { - flags += 'i'; + flags += 'i' } if (regExp.multiline) { - flags += 'm'; + flags += 'm' } if (regExp.unicode) { - flags += 'u'; + flags += 'u' } if (regExp.sticky) { - flags += 'y'; + flags += 'y' } - return flags; -}; + return flags +} -const { isArray } = Array; +const { isArray } = Array const GLOBAL_THIS: FastCopy.Realm = (function () { if (typeof globalThis !== 'undefined') { - return globalThis; + return globalThis } if (typeof self !== 'undefined') { - return self; + return self } if (typeof window !== 'undefined') { - return window; + return window } if (typeof global !== 'undefined') { - return global; + return global } if (console && console.error) { - console.error('Unable to locate global object, returning "this".'); + console.error('Unable to locate global object, returning "this".') } // @ts-ignore - return this; -})(); + return this +})() /** * @function copy @@ -252,8 +252,8 @@ const GLOBAL_THIS: FastCopy.Realm = (function () { */ function copy(value: Value, options?: FastCopy.Options): Value { // manually coalesced instead of default parameters for performance - const realm = (options && options.realm) || GLOBAL_THIS; - const getObjectClone = getObjectCloneStrict; + const realm = (options && options.realm) || GLOBAL_THIS + const getObjectClone = getObjectCloneStrict /** * @function handleCopy @@ -266,103 +266,103 @@ function copy(value: Value, options?: FastCopy.Options): Value { */ const handleCopy: FastCopy.Copier = ( value: any, - cache: FastCopy.Cache, + cache: FastCopy.Cache ): any => { if (!value || typeof value !== 'object') { - return value; + return value } if (cache.has(value)) { - return cache.get(value); + return cache.get(value) } - const prototype = value.__proto__ || getPrototypeOf(value); - const Constructor = prototype && prototype.constructor; + const prototype = value.__proto__ || getPrototypeOf(value) + const Constructor = prototype && prototype.constructor // plain objects if (!Constructor || Constructor === realm.Object) { - return getObjectClone(value, realm, handleCopy, cache); + return getObjectClone(value, realm, handleCopy, cache) } - let clone: any; + let clone: any // arrays if (isArray(value)) { - return getObjectCloneStrict(value, realm, handleCopy, cache); + return getObjectCloneStrict(value, realm, handleCopy, cache) } // dates if (value instanceof realm.Date) { - return new Constructor(value.getTime()); + return new Constructor(value.getTime()) } // regexps if (value instanceof realm.RegExp) { clone = new Constructor( value.source, - value.flags || getRegExpFlags(value), - ); + value.flags || getRegExpFlags(value) + ) - clone.lastIndex = value.lastIndex; + clone.lastIndex = value.lastIndex - return clone; + return clone } // maps if (realm.Map && value instanceof realm.Map) { - clone = new Constructor(); - cache.set(value, clone); + clone = new Constructor() + cache.set(value, clone) value.forEach((value: any, key: any) => { - clone.set(key, handleCopy(value, cache)); - }); + clone.set(key, handleCopy(value, cache)) + }) - return clone; + return clone } // sets if (realm.Set && value instanceof realm.Set) { - clone = new Constructor(); - cache.set(value, clone); + clone = new Constructor() + cache.set(value, clone) value.forEach((value: any) => { - clone.add(handleCopy(value, cache)); - }); + clone.add(handleCopy(value, cache)) + }) - return clone; + return clone } // blobs if (realm.Blob && value instanceof realm.Blob) { - return value.slice(0, value.size, value.type); + return value.slice(0, value.size, value.type) } // buffers (node-only) if (realm.Buffer && realm.Buffer.isBuffer(value)) { clone = realm.Buffer.allocUnsafe ? realm.Buffer.allocUnsafe(value.length) - : new Constructor(value.length); + : new Constructor(value.length) - cache.set(value, clone); - value.copy(clone); + cache.set(value, clone) + value.copy(clone) - return clone; + return clone } // arraybuffers / dataviews if (realm.ArrayBuffer) { // dataviews if (realm.ArrayBuffer.isView(value)) { - clone = new Constructor(value.buffer.slice(0)); - cache.set(value, clone); - return clone; + clone = new Constructor(value.buffer.slice(0)) + cache.set(value, clone) + return clone } // arraybuffers if (value instanceof realm.ArrayBuffer) { - clone = value.slice(0); - cache.set(value, clone); - return clone; + clone = value.slice(0) + cache.set(value, clone) + return clone } } @@ -377,14 +377,14 @@ function copy(value: Value, options?: FastCopy.Options): Value { // weaksets (realm.WeakSet && value instanceof realm.WeakSet) ) { - return value; + return value } // assume anything left is a custom constructor - return getObjectClone(value, realm, handleCopy, cache); - }; + return getObjectClone(value, realm, handleCopy, cache) + } - return handleCopy(value, createCache()); + return handleCopy(value, createCache()) } /** @@ -402,7 +402,7 @@ copy.strict = function strictCopy(value: any, options?: FastCopy.Options) { return copy(value, { isStrict: true, realm: options ? options.realm : void 0, - }); -}; + }) +} -export default copy; \ No newline at end of file +export default copy diff --git a/packages/pinia/src/devtools/plugin.ts b/packages/pinia/src/devtools/plugin.ts index 3da1619902..cabe6216fd 100644 --- a/packages/pinia/src/devtools/plugin.ts +++ b/packages/pinia/src/devtools/plugin.ts @@ -329,7 +329,7 @@ function addStoreToDevtools(app: DevtoolsApp, store: StoreGeneric) { store.$onAction(({ after, onError, name, args }) => { const groupId = runningActionId++ - const initialState = copy(store.$state); + const initialState = copy(store.$state) api.addTimelineEvent({ layerId: MUTATIONS_LAYER_ID, diff --git a/packages/pinia/src/devtools/utils.ts b/packages/pinia/src/devtools/utils.ts index b9dc5262bd..61de32f95d 100644 --- a/packages/pinia/src/devtools/utils.ts +++ b/packages/pinia/src/devtools/utils.ts @@ -29,26 +29,29 @@ export function isPinia(o: any): o is Pinia { } export const realTypeOf = (subject: any) => { - const type = typeof subject; - if (type !== 'object') return type; + const type = typeof subject + if (type !== 'object') return type if (subject === Math) { - return 'math'; + return 'math' } else if (subject === null) { - return 'null'; + return 'null' } else if (Array.isArray(subject)) { - return 'array'; + return 'array' } else if (Object.prototype.toString.call(subject) === '[object Date]') { - return 'date'; - } else if (typeof subject.toString === 'function' && /^\/.*\//.test(subject.toString())) { - return 'regexp'; + return 'date' + } else if ( + typeof subject.toString === 'function' && + /^\/.*\//.test(subject.toString()) + ) { + return 'regexp' } - return 'object'; + return 'object' } export function formatStateDifferences( initialState: StateTree, - newState: StateTree, + newState: StateTree ): StateTree { const stateDifferences: StateTree = {} @@ -87,7 +90,7 @@ export function formatStateDifferences( if (initialState[key].toString() !== newState[key].toString()) { stateDifferences[key] = newState[key] } - break; + break default: if (initialState[key] !== newState[key]) { stateDifferences[key] = newState[key] @@ -95,11 +98,11 @@ export function formatStateDifferences( } } - Object.keys(initialState).forEach(key => { + Object.keys(initialState).forEach((key) => { if (!(key in newState)) { stateDifferences[key] = undefined } }) return stateDifferences -} \ No newline at end of file +}