diff --git a/jest.config.js b/jest.config.js index ddff3e15..911c51e0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,8 +26,10 @@ module.exports = { testEnvironment: 'jest-environment-jsdom-sixteen', testMatch: [ + // '/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx', + // '/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx', '/scripts/__tests__/**/*.test.js', - '/scripts/__tests__/**/*.test.tsx' + '/scripts/__tests__/**/*.test.tsx', ], timers: 'fake', diff --git a/libs/horizon/src/horizonx/CommonUtils.ts b/libs/horizon/src/horizonx/CommonUtils.ts index e6387b6a..cf2c2bb8 100644 --- a/libs/horizon/src/horizonx/CommonUtils.ts +++ b/libs/horizon/src/horizonx/CommonUtils.ts @@ -84,7 +84,6 @@ export function isSame(x, y) { export function getDetailedType(val: any) { if (val === undefined) return 'undefined'; if (val === null) return 'null'; - if (isCollection(val)) return 'collection'; if (isPromise(val)) return 'promise'; if (isArray(val)) return 'array'; if (isWeakMap(val)) return 'weakMap'; @@ -121,7 +120,24 @@ export function resolveMutation(from, to) { } case 'object': { - let keys = Object.keys({ ...from, ...to }); + if (from._type && from._type === to._type) { + if (from._type === 'Map') { + const entries = resolveMutation(from.entries, to.entries); + return { + mutation: entries.items.some(item => item.mutation), + from, + to, + entries: entries.items, + }; + } + + if (from._type === 'Set') { + const values = resolveMutation(from.values, to.values); + return { mutation: values.items.some(item => item.mutation), from, to, values: values.items }; + } + } + + let keys = Object.keys({ ...from, ...to }).filter(key => key !== '_horizonObserver'); const res = {}; let found = false; keys.forEach(key => { @@ -142,8 +158,6 @@ export function resolveMutation(from, to) { return { mutation: found, attributes: res, from, to }; } - // TODO: implement collections - default: { if (from === to) return { mutation: false }; @@ -151,3 +165,9 @@ export function resolveMutation(from, to) { } } } + +export function omit(obj, ...attrs) { + let res = { ...obj }; + attrs.forEach(attr => delete res[attr]); + return res; +} diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index e7f3514e..192ece34 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -1,3 +1,5 @@ +import { isDomVNode } from '../../renderer/vnode/VNodeUtils'; +import { isMap, isSet, isWeakMap, isWeakSet } from '../CommonUtils'; import { getStore, getAllStores } from '../store/StoreHandler'; import { OBSERVED_COMPONENTS } from './constants'; @@ -24,28 +26,87 @@ function makeStoreSnapshot({ type, data }) { } // safely serializes variables containing values wrapped in Proxy object +function getType(value) { + if (!value) return 'nullish'; + if (value.nativeEvent) return 'event'; + if (typeof value === 'function') return 'function'; + if (value.constructor?.name === 'VNode') return 'vnode'; + if (isWeakMap(value)) return 'weakMap'; + if (isWeakSet(value)) return 'weakSet'; + if (isMap(value)) return 'map'; + if (isSet(value)) return 'set'; + if (Array.isArray(value)) return 'array'; + if (typeof value === 'object') return 'object'; + return 'primitive'; +} function makeProxySnapshot(obj) { + const type = getType(obj); let clone; + try { - if (!obj) { + //NULLISH VALUE + if (type === 'nullish') { return obj; } - if (obj.nativeEvent) return obj.type + 'Event'; - if (typeof obj === 'function') { + //EVENT + if (type === 'event') return obj.type + 'Event'; + // FUNCTION + if (type === 'function') { return obj.toString(); } - if (Array.isArray(obj)) { + // VNODE + if (type === 'vnode') { + return { + _type: 'VNode', + id: window['__HORIZON_DEV_HOOK__'].getVnodeId(obj), + tag: obj.tag, + }; + } + // WEAK MAP + if (type === 'weakMap') { + return { + _type: 'WeakMap', + }; + } + // WEAK SET + if (type === 'weakSet') { + return { + _type: 'WeakSet', + }; + } + // MAP + if (type === 'map') { + return { + _type: 'Map', + entries: Array.from(obj.entries()).map(([key, value]) => ({ + key: makeProxySnapshot(key), + value: makeProxySnapshot(value), + })), + }; + } + // SET + if (type === 'set') { + return { + _type: 'Set', + values: Array.from(obj).map(value => makeProxySnapshot(value)), + }; + } + // ARRAY + if (type === 'array') { clone = []; obj.forEach(item => clone.push(makeProxySnapshot(item))); return clone; - } else if (typeof obj === 'object') { + } + // OBJECT + if (type === 'object') { clone = {}; Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value))); return clone; } + // PRIMITIVE return obj; } catch (err) { - throw console.log('cannot serialize object. ' + err); + console.error('cannot serialize object. ', { err, obj, type }); } } @@ -76,7 +137,7 @@ function getAffectedComponents() { const subRes = new Set(); const process = Array.from(allStores[key].$config.state._horizonObserver.keyVNodes.values()); while (process.length) { - let pivot = process.shift(); + let pivot = process.shift() as { tag: 'string' }; if (pivot?.tag) subRes.add(pivot); if (pivot?.toString() === '[object Set]') Array.from(pivot).forEach(item => process.push(item)); } @@ -117,4 +178,37 @@ window.addEventListener('message', messageEvent => { const params = data.params; action(...params); } + + // queues store action + if (messageEvent.data.payload.type === 'horizonx queue action') { + const data = messageEvent.data.payload.data; + const store = getStore(data.storeId); + if (!store?.[data.action]) return; + + const action = store.$queue?.[data.action]; + const params = data.params; + action(...params); + } + + // queues change store state + if (messageEvent.data.payload.type === 'horizonx change state') { + const data = messageEvent.data.payload; + const store = getStore(data.storeId); + if (!store) return; + let parent = store.$s; + if (data.operation === 'edit') { + try { + const path = messageEvent.data.payload.path; + + while (path.length > 1) { + parent = parent[path.pop()]; + } + + parent[path[0]] = messageEvent.data.payload.value; + } catch (err) { + console.error(err); + } + } + // TODO:implement add and delete element + } }); diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index dd5015e9..a47ad752 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -31,6 +31,10 @@ export class HooklessObserver implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } + getListeners() { + return this.listeners; + } + setProp(key: string | symbol, mutation: any): void { this.triggerChangeListeners(mutation); } diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index fda6d653..d165675e 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -27,7 +27,7 @@ const proxyMap = new WeakMap(); export const hookObserverMap = new WeakMap(); -export function createProxy(rawObj: any, id, isHookObserver = true): any { +export function createProxy(rawObj: any, isHookObserver = true, listener: { current: (...args) => any }): any { // 不是对象(是原始数据类型)不用代理 if (!(rawObj && isObject(rawObj))) { return rawObj; @@ -56,16 +56,32 @@ export function createProxy(rawObj: any, id, isHookObserver = true): any { // 创建Proxy let proxyObj; if (!isHookObserver) { - proxyObj = createObjectProxy(rawObj, true); + proxyObj = createObjectProxy(rawObj, true, { + current: change => { + listener.current(change); + }, + }); } else if (isArray(rawObj)) { // 数组 - proxyObj = createArrayProxy(rawObj as []); + proxyObj = createArrayProxy(rawObj as [], { + current: change => { + listener.current(change); + }, + }); } else if (isCollection(rawObj)) { // 集合 - proxyObj = createCollectionProxy(rawObj); + proxyObj = createCollectionProxy(rawObj, true, { + current: change => { + listener.current(change); + }, + }); } else { // 原生对象 或 函数 - proxyObj = createObjectProxy(rawObj); + proxyObj = createObjectProxy(rawObj, false, { + current: change => { + listener?.current(change); + }, + }); } proxyMap.set(rawObj, proxyObj); diff --git a/libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts new file mode 100644 index 00000000..9e9c2845 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getObserver } from '../ProxyHandler'; +import { isSame, isValidIntegerKey } from '../../CommonUtils'; +import { get as objectGet } from './ObjectProxyHandler'; +import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; + +export function createArrayProxy(rawObj: any[]): any[] { + const handle = { + get, + set, + }; + + return new Proxy(rawObj, handle); +} + +function get(rawObj: any[], key: string, receiver: any) { + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (isValidIntegerKey(key) || key === 'length') { + return objectGet(rawObj, key, receiver); + } + + return Reflect.get(rawObj, key, receiver); +} + +function set(rawObj: any[], key: string, value: any, receiver: any) { + const oldValue = rawObj[key]; + const oldLength = rawObj.length; + const newValue = value; + + const oldArray = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + + const ret = Reflect.set(rawObj, key, newValue, receiver); + + const newLength = rawObj.length; + const observer = getObserver(rawObj); + + const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : { mutation: true, from: [], to: rawObj }; + + if (!isSame(newValue, oldValue)) { + // 值不一样,触发监听器 + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue, mutation); + }); + } + + // 触发属性变化 + observer.setProp(key, mutation); + } + + if (oldLength !== newLength) { + // 触发数组的大小变化 + observer.setProp('length', mutation); + } + + return ret; +} diff --git a/libs/horizon/src/horizonx/proxy/handlers copy/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers copy/CollectionProxyHandler.ts new file mode 100644 index 00000000..05174cc5 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers copy/CollectionProxyHandler.ts @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { isMap, isWeakMap, isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; + +const COLLECTION_CHANGE = '_collectionChange'; +const handler = { + get, + set, + add, + delete: deleteFun, + clear, + has, + entries, + forEach, + keys, + values, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf, +}; + +export function createCollectionProxy(rawObj: Object, hookObserver = true): Object { + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + return new Proxy(rawObj, { ...boundHandler }); +} + +function get(rawObj: { size: number }, key: any, receiver: any): any { + if (key === 'size') { + return size(rawObj); + } else if (key === 'get') { + return getFun.bind(null, rawObj); + } else if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } else if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + return Reflect.get(rawObj, key, receiver); +} + +function getFun(rawObj: { get: (key: any) => any }, key: any) { + const observer = getObserver(rawObj); + observer.useProp(key); + + const value = rawObj.get(key); + // 对于value也需要进一步代理 + const valProxy = createProxy(value, hookObserverMap.get(rawObj)); + + return valProxy; +} + +// Map的set方法 +function set( + rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean }, + key: any, + value: any +) { + const oldValue = rawObj.get(key); + const newValue = value; + rawObj.set(key, newValue); + const valChange = !isSame(newValue, oldValue); + const observer = getObserver(rawObj); + + const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : { mutation: true, from: null, to: rawObj }; + + if (valChange || !rawObj.has(key)) { + observer.setProp(COLLECTION_CHANGE, mutation); + } + + if (valChange) { + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue, mutation); + }); + } + + observer.setProp(key, mutation); + } + + return rawObj; +} + +// Set的add方法 +function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + if (!rawObj.has(value)) { + rawObj.add(value); + + const observer = getObserver(rawObj); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + } + + return rawObj; +} + +function has(rawObj: { has: (string) => boolean }, key: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(key); + + return rawObj.has(key); +} + +function clear(rawObj: { size: number; clear: () => void }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const observer = getObserver(rawObj); + observer.allChange(); + } +} + +function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + if (rawObj.has(key)) { + rawObj.delete(key); + + const observer = getObserver(rawObj); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + + return true; + } + + return false; +} + +function size(rawObj: { size: number }) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + return rawObj.size; +} + +function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.keys()); +} + +function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.values()); +} + +function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.entries(), true); +} + +function forOf(rawObj: { + entries: () => { next: () => { value: any; done: boolean } }; + values: () => { next: () => { value: any; done: boolean } }; +}) { + const isMapType = isMap(rawObj) || isWeakMap(rawObj); + const iterator = isMapType ? rawObj.entries() : rawObj.values(); + return wrapIterator(rawObj, iterator, isMapType); +} + +function forEach( + rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, + callback: (valProxy: any, keyProxy: any, rawObj: any) => void +) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + rawObj.forEach((value, key) => { + const valProxy = createProxy(value, hookObserverMap.get(rawObj)); + const keyProxy = createProxy(key, hookObserverMap.get(rawObj)); + // 最后一个参数要返回代理对象 + return callback(valProxy, keyProxy, rawObj); + }); +} + +function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, isPair = false) { + const observer = getObserver(rawObj); + const hookObserver = hookObserverMap.get(rawObj); + observer.useProp(COLLECTION_CHANGE); + + return { + next() { + const { value, done } = rawIt.next(); + if (done) { + return { value: createProxy(value, hookObserver), done }; + } + + observer.useProp(COLLECTION_CHANGE); + + let newVal; + if (isPair) { + newVal = [createProxy(value[0], hookObserver), createProxy(value[1], hookObserver)]; + } else { + newVal = createProxy(value, hookObserver); + } + + return { value: newVal, done }; + }, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() { + return this; + }, + }; +} diff --git a/libs/horizon/src/horizonx/proxy/handlers copy/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers copy/ObjectProxyHandler.ts new file mode 100644 index 00000000..721be056 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers copy/ObjectProxyHandler.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { isSame, resolveMutation } from '../../CommonUtils'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { OBSERVER_KEY } from '../../Constants'; +import { isPanelActive } from '../../devtools'; + +export function createObjectProxy(rawObj: T, singleLevel = false): ProxyHandler { + const proxy = new Proxy(rawObj, { + get: (...args) => get(...args, singleLevel), + set, + }); + + return proxy; +} + +export function get(rawObj: object, key: string | symbol, receiver: any, singleLevel = false): any { + // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. + if (key === OBSERVER_KEY) { + return undefined; + } + + const observer = getObserver(rawObj); + + if (key === 'watch') { + return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return observer.addListener.bind(observer); + } + + if (key === 'removeListener') { + return observer.removeListener.bind(observer); + } + + observer.useProp(key); + + const value = Reflect.get(rawObj, key, receiver); + + // 对于prototype不做代理 + if (key !== 'prototype') { + // 对于value也需要进一步代理 + const valProxy = singleLevel ? value : createProxy(value, hookObserverMap.get(rawObj)); + + return valProxy; + } + + return value; +} + +export function set(rawObj: object, key: string, value: any, receiver: any): boolean { + const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + const observer = getObserver(rawObj); + + const oldValue = rawObj[key]; + const newValue = value; + + const ret = Reflect.set(rawObj, key, newValue, receiver); + const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : { mutation: true, from: null, to: rawObj }; + + if (!isSame(newValue, oldValue)) { + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue, mutation); + }); + } + observer.setProp(key, mutation); + } + return ret; +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 9e9c2845..3938ecab 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -13,43 +13,112 @@ * See the Mulan PSL v2 for more details. */ -import { getObserver } from '../ProxyHandler'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { isSame, isValidIntegerKey } from '../../CommonUtils'; -import { get as objectGet } from './ObjectProxyHandler'; import { resolveMutation } from '../../CommonUtils'; import { isPanelActive } from '../../devtools'; +import { OBSERVER_KEY } from '../../Constants'; + +export function createArrayProxy(rawObj: any[], listener: { current: (...args) => any }): any[] { + let listeners = [] as ((...args) => void)[]; + + function objectGet(rawObj: object, key: string | symbol, receiver: any, singleLevel = false): any { + // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. + if (key === OBSERVER_KEY) { + return undefined; + } + + const observer = getObserver(rawObj); + + if (key === 'watch') { + return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + observer.useProp(key); + + const value = Reflect.get(rawObj, key, receiver); + + // 对于prototype不做代理 + if (key !== 'prototype') { + // 对于value也需要进一步代理 + const valProxy = singleLevel + ? value + : createProxy(value, hookObserverMap.get(rawObj), { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current(mutation); + listeners.forEach(lst => lst(mutation)); + }, + }); + + return valProxy; + } + + return value; + } + + function get(rawObj: any[], key: string, receiver: any) { + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (isValidIntegerKey(key) || key === 'length') { + return objectGet(rawObj, key, receiver); + } + + return Reflect.get(rawObj, key, receiver); + } -export function createArrayProxy(rawObj: any[]): any[] { const handle = { get, set, }; + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + return new Proxy(rawObj, handle); } -function get(rawObj: any[], key: string, receiver: any) { - if (key === 'watch') { - const observer = getObserver(rawObj); - - return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { - if (!observer.watchers[prop]) { - observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; - } - observer.watchers[prop].push(handler); - return () => { - observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); - }; - }; - } - - if (isValidIntegerKey(key) || key === 'length') { - return objectGet(rawObj, key, receiver); - } - - return Reflect.get(rawObj, key, receiver); -} - function set(rawObj: any[], key: string, value: any, receiver: any) { const oldValue = rawObj[key]; const oldLength = rawObj.length; @@ -62,7 +131,7 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const newLength = rawObj.length; const observer = getObserver(rawObj); - const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : { mutation: true, from: [], to: rawObj }; + const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : resolveMutation(null, rawObj); if (!isSame(newValue, oldValue)) { // 值不一样,触发监听器 diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index 05174cc5..1b0f0d37 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -13,223 +13,25 @@ * See the Mulan PSL v2 for more details. */ -import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; -import { isMap, isWeakMap, isSame } from '../../CommonUtils'; -import { resolveMutation } from '../../CommonUtils'; -import { isPanelActive } from '../../devtools'; +import { isWeakMap, isWeakSet, isSet } from '../../CommonUtils'; +import { createWeakSetProxy } from './WeakSetProxy'; +import { createSetProxy } from './SetProxy'; +import { createWeakMapProxy } from './WeakMapProxy'; +import { createMapProxy } from './MapProxy'; -const COLLECTION_CHANGE = '_collectionChange'; -const handler = { - get, - set, - add, - delete: deleteFun, - clear, - has, - entries, - forEach, - keys, - values, - // 判断Symbol类型,兼容IE - [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf, -}; - -export function createCollectionProxy(rawObj: Object, hookObserver = true): Object { - const boundHandler = {}; - Object.entries(handler).forEach(([id, val]) => { - boundHandler[id] = (...args: any[]) => { - return (val as any)(...args, hookObserver); - }; - }); - return new Proxy(rawObj, { ...boundHandler }); -} - -function get(rawObj: { size: number }, key: any, receiver: any): any { - if (key === 'size') { - return size(rawObj); - } else if (key === 'get') { - return getFun.bind(null, rawObj); - } else if (Object.prototype.hasOwnProperty.call(handler, key)) { - const value = Reflect.get(handler, key, receiver); - return value.bind(null, rawObj); - } else if (key === 'watch') { - const observer = getObserver(rawObj); - - return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { - if (!observer.watchers[prop]) { - observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; - } - observer.watchers[prop].push(handler); - return () => { - observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); - }; - }; +export function createCollectionProxy( + rawObj: Object, + hookObserver = true, + listener: { current: (...args) => any } +): Object { + if (isWeakSet(rawObj)) { + return createWeakSetProxy(rawObj, hookObserver, listener); } - - return Reflect.get(rawObj, key, receiver); -} - -function getFun(rawObj: { get: (key: any) => any }, key: any) { - const observer = getObserver(rawObj); - observer.useProp(key); - - const value = rawObj.get(key); - // 对于value也需要进一步代理 - const valProxy = createProxy(value, hookObserverMap.get(rawObj)); - - return valProxy; -} - -// Map的set方法 -function set( - rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean }, - key: any, - value: any -) { - const oldValue = rawObj.get(key); - const newValue = value; - rawObj.set(key, newValue); - const valChange = !isSame(newValue, oldValue); - const observer = getObserver(rawObj); - - const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : { mutation: true, from: null, to: rawObj }; - - if (valChange || !rawObj.has(key)) { - observer.setProp(COLLECTION_CHANGE, mutation); + if (isSet(rawObj)) { + return createSetProxy(rawObj, hookObserver, listener); } - - if (valChange) { - if (observer.watchers?.[key]) { - observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue, mutation); - }); - } - - observer.setProp(key, mutation); + if (isWeakMap(rawObj)) { + return createWeakMapProxy(rawObj, hookObserver, listener); } - - return rawObj; -} - -// Set的add方法 -function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { - const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; - if (!rawObj.has(value)) { - rawObj.add(value); - - const observer = getObserver(rawObj); - const mutation = isPanelActive() - ? resolveMutation(oldCollection, rawObj) - : { mutation: true, from: null, to: rawObj }; - observer.setProp(value, mutation); - observer.setProp(COLLECTION_CHANGE, mutation); - } - - return rawObj; -} - -function has(rawObj: { has: (string) => boolean }, key: any): boolean { - const observer = getObserver(rawObj); - observer.useProp(key); - - return rawObj.has(key); -} - -function clear(rawObj: { size: number; clear: () => void }) { - const oldSize = rawObj.size; - rawObj.clear(); - - if (oldSize > 0) { - const observer = getObserver(rawObj); - observer.allChange(); - } -} - -function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { - const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; - if (rawObj.has(key)) { - rawObj.delete(key); - - const observer = getObserver(rawObj); - const mutation = isPanelActive() - ? resolveMutation(oldCollection, rawObj) - : { mutation: true, from: null, to: rawObj }; - observer.setProp(key, mutation); - observer.setProp(COLLECTION_CHANGE, mutation); - - return true; - } - - return false; -} - -function size(rawObj: { size: number }) { - const observer = getObserver(rawObj); - observer.useProp(COLLECTION_CHANGE); - return rawObj.size; -} - -function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) { - return wrapIterator(rawObj, rawObj.keys()); -} - -function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) { - return wrapIterator(rawObj, rawObj.values()); -} - -function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) { - return wrapIterator(rawObj, rawObj.entries(), true); -} - -function forOf(rawObj: { - entries: () => { next: () => { value: any; done: boolean } }; - values: () => { next: () => { value: any; done: boolean } }; -}) { - const isMapType = isMap(rawObj) || isWeakMap(rawObj); - const iterator = isMapType ? rawObj.entries() : rawObj.values(); - return wrapIterator(rawObj, iterator, isMapType); -} - -function forEach( - rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, - callback: (valProxy: any, keyProxy: any, rawObj: any) => void -) { - const observer = getObserver(rawObj); - observer.useProp(COLLECTION_CHANGE); - rawObj.forEach((value, key) => { - const valProxy = createProxy(value, hookObserverMap.get(rawObj)); - const keyProxy = createProxy(key, hookObserverMap.get(rawObj)); - // 最后一个参数要返回代理对象 - return callback(valProxy, keyProxy, rawObj); - }); -} - -function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, isPair = false) { - const observer = getObserver(rawObj); - const hookObserver = hookObserverMap.get(rawObj); - observer.useProp(COLLECTION_CHANGE); - - return { - next() { - const { value, done } = rawIt.next(); - if (done) { - return { value: createProxy(value, hookObserver), done }; - } - - observer.useProp(COLLECTION_CHANGE); - - let newVal; - if (isPair) { - newVal = [createProxy(value[0], hookObserver), createProxy(value[1], hookObserver)]; - } else { - newVal = createProxy(value, hookObserver); - } - - return { value: newVal, done }; - }, - // 判断Symbol类型,兼容IE - [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() { - return this; - }, - }; + return createMapProxy(rawObj, hookObserver, listener); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/MapProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/MapProxy.ts new file mode 100644 index 00000000..d5ed5d37 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/MapProxy.ts @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; + +const COLLECTION_CHANGE = '_collectionChange'; + +export function createMapProxy(rawObj: Object, hookObserver = true, listener: { current: (...args) => any }): Object { + let listeners: ((mutation) => {})[] = []; + let oldData: [any, any][] = []; + let proxies = new Map(); + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (key === 'size') { + return size(rawObj); + } + + if (key === 'get') { + return getFun.bind(null, rawObj); + } + + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + return Reflect.get(rawObj, key, receiver); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function getFun(rawObj: { get: (key: any) => any; has: (key: any) => boolean }, key: any) { + const keyProxy = rawObj.has(key) ? key : proxies.get(key); + if (!keyProxy) return; + const observer = getObserver(rawObj); + observer.useProp(key); + const value = rawObj.get(keyProxy); + + // 对于value也需要进一步代理 + const valProxy = createProxy(value, hookObserverMap.get(rawObj), { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + + return valProxy; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Map的set方法 + function set( + rawObj: { + get: (key: any) => any; + set: (key: any, value: any) => any; + has: (key: any) => boolean; + entries: () => [any, any][]; + }, + key: any, + value: any + ) { + if (rawObj.has(key) || rawObj.has(proxies.get(key))) { + // VALUE CHANGE (whole value for selected key is changed) + const oldValue = rawObj.get(proxies.get(key)); + if (isSame(value, oldValue)) return; + rawObj.set(proxies.get(key), value); + const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj); + const observer = getObserver(rawObj); + observer.setProp(COLLECTION_CHANGE, mutation); + + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, oldValue, value, mutation); + }); + } + + observer.setProp(key, mutation); + oldData = [...Array.from(rawObj.entries())]; + } else { + // NEW VALUE + const keyProxy = createProxy(key, hookObserverMap.get(rawObj), { + current: change => { + // KEY CHANGE + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, ['_keyChange']: change.mutation.from }, + { ...rawObj, ['_keyChange']: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + proxies.set(key, keyProxy); + + rawObj.set(keyProxy, value); + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Map', + entries: oldData, + }, + { + _type: 'Map', + entries: Array.from(rawObj.entries()), + } + ); + observer.setProp(COLLECTION_CHANGE, mutation); + + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, null, value, mutation); + }); + } + observer.setProp(key, mutation); + oldData = [...Array.from(rawObj.entries())]; + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (any) => boolean }, key: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(key); + if (rawObj.has(key)) { + return true; + } + return proxies.has(key); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function clear(rawObj: { size: number; clear: () => void; entries: () => [any, any][] }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const observer = getObserver(rawObj); + observer.allChange(); + oldData = [...Array.from(rawObj.entries())]; + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun( + rawObj: { has: (key: any) => boolean; delete: (key: any) => void; entries: () => [any, any][] }, + key: any + ) { + if (rawObj.has(key) || proxies.has(key)) { + rawObj.delete(key || proxies.get(key)); + + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Map', + entries: oldData, + }, + { + _type: 'Map', + entries: Array.from(rawObj.entries()), + } + ); + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + + oldData = [...Array.from(rawObj.entries())]; + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function size(rawObj: { size: number }) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + return rawObj.size; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.keys(), 'keys'); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.values(), 'values'); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.entries(), 'entries'); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function forOf(rawObj: { + entries: () => { next: () => { value: any; done: boolean } }; + values: () => { next: () => { value: any; done: boolean } }; + }) { + return wrapIterator(rawObj, rawObj.entries(), 'entries'); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function forEach( + rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, + callback: (valProxy: any, keyProxy: any, rawObj: any) => void + ) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + rawObj.forEach((value, key) => { + const keyProxy = createProxy(value, hookObserverMap.get(rawObj), { + current: change => { + //KEY ATTRIBUTES CHANGED + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, ['_keyChange']: change.mutation.from }, + { ...rawObj, ['_keyChange']: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + const valProxy = createProxy(key, hookObserverMap.get(rawObj), { + current: change => { + // VALUE ATTRIBUTE CHANGED + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, key: change.mutation.from }, + { ...rawObj, key: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + // 最后一个参数要返回代理对象 + return callback(keyProxy, valProxy, rawObj); + }); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, type) { + const observer = getObserver(rawObj); + const hookObserver = hookObserverMap.get(rawObj); + observer.useProp(COLLECTION_CHANGE); + + return { + next() { + const { value, done } = rawIt.next(); + if (done) { + return { + value: createProxy(value, hookObserver, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [value]: change.mutation.from }, + { ...rawObj, [value]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }), + done, + }; + } + + observer.useProp(COLLECTION_CHANGE); + let newVal; + if (type === 'entries') { + //ENTRY CHANGED + newVal = [ + createProxy(value[0], hookObserver, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, ['itemChange']: { key: change.mutation.from, value: value[1] } }, + { ...rawObj, ['itemChange']: { key: change.mutation.to, value: value[1] } } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }), + createProxy(value[1], hookObserver, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, item: { key: value[0], value: change.mutation.from } }, + { ...rawObj, item: { key: value[0], value: change.mutation.to } } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }), + ]; + } else { + // SINGLE VALUE CHANGED + newVal = createProxy(value, hookObserver, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.from }, + { ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + } + + return { value: newVal, done }; + }, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() { + return this; + }, + }; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const handler = { + get, + set, + delete: deleteFun, + clear, + has, + entries, + forEach, + keys, + values, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf, + }; + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 80da37fd..f1ac97b7 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -18,70 +18,97 @@ import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; import { isPanelActive } from '../../devtools'; -export function createObjectProxy(rawObj: T, singleLevel = false): ProxyHandler { +export function createObjectProxy( + rawObj: T, + singleLevel = false, + listener: { current: (...args) => any } +): ProxyHandler { + let listeners = [] as ((...args) => void)[]; + + function get(rawObj: object, key: string | symbol, receiver: any): any { + // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. + if (key === OBSERVER_KEY) { + return undefined; + } + + const observer = getObserver(rawObj); + + if (key === 'watch') { + return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + observer.useProp(key); + + const value = Reflect.get(rawObj, key, receiver); + + // 对于prototype不做代理 + if (key !== 'prototype') { + // 对于value也需要进一步代理 + const valProxy = singleLevel + ? value + : createProxy(value, hookObserverMap.get(rawObj), { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + + return valProxy; + } + + return value; + } + const proxy = new Proxy(rawObj, { - get: (...args) => get(...args, singleLevel), + get, set, }); + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + return proxy; } -export function get(rawObj: object, key: string | symbol, receiver: any, singleLevel = false): any { - // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. - if (key === OBSERVER_KEY) { - return undefined; - } - - const observer = getObserver(rawObj); - - if (key === 'watch') { - return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => { - if (!observer.watchers[prop]) { - observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; - } - observer.watchers[prop].push(handler); - return () => { - observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); - }; - }; - } - - if (key === 'addListener') { - return observer.addListener.bind(observer); - } - - if (key === 'removeListener') { - return observer.removeListener.bind(observer); - } - - observer.useProp(key); - - const value = Reflect.get(rawObj, key, receiver); - - // 对于prototype不做代理 - if (key !== 'prototype') { - // 对于value也需要进一步代理 - const valProxy = singleLevel ? value : createProxy(value, hookObserverMap.get(rawObj)); - - return valProxy; - } - - return value; -} - -export function set(rawObj: object, key: string, value: any, receiver: any): boolean { +function set(rawObj: object, key: string, value: any, receiver: any): boolean { const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; const observer = getObserver(rawObj); - if (value && key == 'removeListener') { - observer.removeListener(value); - } const oldValue = rawObj[key]; const newValue = value; const ret = Reflect.set(rawObj, key, newValue, receiver); - const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : { mutation: true, from: null, to: rawObj }; + const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : resolveMutation(null, rawObj); if (!isSame(newValue, oldValue)) { if (observer.watchers?.[key]) { diff --git a/libs/horizon/src/horizonx/proxy/handlers/SetProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/SetProxy.ts new file mode 100644 index 00000000..6a4401fa --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/SetProxy.ts @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { resolveMutation } from '../../CommonUtils'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; + +const COLLECTION_CHANGE = '_collectionChange'; + +export function createSetProxy( + rawObj: T, + hookObserver = true, + listener: { current: (...args) => any } +): ProxyHandler { + let listeners: ((mutation) => {})[] = []; + let proxies = new WeakMap(); + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + + if (key === 'size') { + return size(rawObj); + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + return Reflect.get(rawObj, key, receiver); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Set的add方法 + function add(rawObj: { add: (any) => void; has: (any) => boolean; values: () => any[] }, value: any): Object { + if (!rawObj.has(proxies.get(value))) { + const proxy = createProxy(value, hookObserverMap.get(rawObj), { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, valueChange: change.mutation.from }, + { ...rawObj, valueChange: change.mutation.to } + ); + listener.current({ + ...change, + mutation, + }); + listeners.forEach(lst => + lst({ + ...change, + mutation, + }) + ); + }, + }); + const oldValues = Array.from(rawObj.values()); + + proxies.set(value, proxy); + + rawObj.add(proxies.get(value)); + + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Set', + values: oldValues, + }, + { + _type: 'Set', + values: Array.from(rawObj.values()), + } + ); + + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (string) => boolean }, value: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(value); + + return rawObj.has(proxies.get(value)); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun( + rawObj: { has: (key: any) => boolean; delete: (value: any) => void; values: () => any[] }, + value: any + ) { + const val = rawObj.has(proxies.get(value)) ? proxies.get(value) : value; + if (rawObj.has(val)) { + const oldValues = Array.from(rawObj.values()); + rawObj.delete(val); + + proxies.delete(value); + + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Set', + values: oldValues, + }, + { + _type: 'Set', + values: Array.from(rawObj.values()), + } + ); + + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function clear(rawObj: { size: number; clear: () => void }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const observer = getObserver(rawObj); + observer.allChange(); + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function size(rawObj: { size: number }) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + return rawObj.size; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.keys()); + } + + function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.values()); + } + + function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.entries()); + } + + function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }) { + const observer = getObserver(rawObj); + const hookObserver = hookObserverMap.get(rawObj); + observer.useProp(COLLECTION_CHANGE); + + return { + next() { + const currentListener = { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, valueChange: change.mutation.from }, + { ...rawObj, valueChange: change.mutation.to } + ); + listener.current({ + ...change, + mutation, + }); + listeners.forEach(lst => + lst({ + ...change, + mutation, + }) + ); + }, + }; + const { value, done } = rawIt.next(); + if (done) { + return { value: createProxy(value, hookObserver, currentListener), done }; + } + + observer.useProp(COLLECTION_CHANGE); + + let newVal; + newVal = createProxy(value, hookObserver, currentListener); + + return { value: newVal, done }; + }, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() { + return this; + }, + }; + } + + function forOf(rawObj: { + entries: () => { next: () => { value: any; done: boolean } }; + values: () => { next: () => { value: any; done: boolean } }; + }) { + const iterator = rawObj.values(); + return wrapIterator(rawObj, iterator); + } + + function forEach( + rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, + callback: (valProxy: any, keyProxy: any, rawObj: any) => void + ) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + rawObj.forEach((value, key) => { + const currentListener = { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, valueChange: change.mutation.from }, + { ...rawObj, valueChange: change.mutation.to } + ); + listener.current({ + ...change, + mutation, + }); + listeners.forEach(lst => + lst({ + ...change, + mutation, + }) + ); + }, + }; + const valProxy = createProxy(value, hookObserverMap.get(rawObj), currentListener); + const keyProxy = createProxy(key, hookObserverMap.get(rawObj), currentListener); + // 最后一个参数要返回代理对象 + return callback(valProxy, keyProxy, rawObj); + }); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const handler = { + get, + add, + delete: deleteFun, + has, + clear, + forEach, + forOf, + entries, + keys, + values, + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf, + }; + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/WeakMapProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/WeakMapProxy.ts new file mode 100644 index 00000000..d5f87ff3 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/WeakMapProxy.ts @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; + +const COLLECTION_CHANGE = '_collectionChange'; + +export function createWeakMapProxy( + rawObj: Object, + hookObserver = true, + listener: { current: (...args) => any } +): Object { + let listeners: ((mutation) => {})[] = []; + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (key === 'get') { + return getFun.bind(null, rawObj); + } + + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + return Reflect.get(rawObj, key, receiver); + } + + function getFun(rawObj: { get: (key: any) => any }, key: any) { + const observer = getObserver(rawObj); + observer.useProp(key); + + const value = rawObj.get(key); + // 对于value也需要进一步代理 + const valProxy = createProxy(value, hookObserverMap.get(rawObj), { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + + return valProxy; + } + + // Map的set方法 + function set( + rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean }, + key: any, + value: any + ) { + const oldValue = rawObj.get(key); + const newValue = value; + rawObj.set(key, newValue); + const valChange = !isSame(newValue, oldValue); + const observer = getObserver(rawObj); + + const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj); + + if (valChange || !rawObj.has(key)) { + observer.setProp(COLLECTION_CHANGE, mutation); + } + + if (valChange) { + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue, mutation); + }); + } + + observer.setProp(key, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Set的add方法 + function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + if (!rawObj.has(value)) { + rawObj.add(value); + + const observer = getObserver(rawObj); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (string) => boolean }, key: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(key); + + return rawObj.has(key); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function clear(rawObj: { size: number; clear: () => void }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const observer = getObserver(rawObj); + observer.allChange(); + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + if (rawObj.has(key)) { + rawObj.delete(key); + + const observer = getObserver(rawObj); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const handler = { + get, + set, + add, + delete: deleteFun, + clear, + has, + }; + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/WeakSetProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/WeakSetProxy.ts new file mode 100644 index 00000000..27becbe9 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/WeakSetProxy.ts @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { resolveMutation } from '../../CommonUtils'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; + +export function createWeakSetProxy( + rawObj: T, + hookObserver = true, + listener: { current: (...args) => any } +): ProxyHandler { + let listeners: ((mutation) => {})[] = []; + let proxies = new WeakMap(); + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + return Reflect.get(rawObj, key, receiver); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Set的add方法 + function add(rawObj: { add: (any) => void; has: (any) => boolean }, value: any): Object { + if (!rawObj.has(proxies.get(value))) { + const proxy = createProxy(value, hookObserverMap.get(rawObj), { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [value]: change.mutation.from }, + { ...rawObj, [value]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }); + + proxies.set(value, proxy); + + rawObj.add(proxies.get(value)); + + const observer = getObserver(rawObj); + const mutation = { mutation: true, from: rawObj, to: value }; + + observer.setProp(value, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (string) => boolean }, value: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(value); + + return rawObj.has(proxies.get(value)); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun(rawObj: { has: (key: any) => boolean; delete: (value: any) => void }, value: any) { + if (rawObj.has(proxies.get(value))) { + rawObj.delete(proxies.get(value)); + + proxies.delete(value); + + const observer = getObserver(rawObj); + const mutation = { mutation: true, from: value, to: rawObj }; + + observer.setProp(value, mutation); + + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const handler = { + get, + add, + delete: deleteFun, + has, + }; + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 8b3aa154..781deef4 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -38,6 +38,7 @@ import { ACTION_QUEUED, INITIALIZED, QUEUE_FINISHED, + QUEUE_PENDING, STATE_CHANGE, SUBSCRIBED, UNSUBSCRIBED, @@ -62,7 +63,11 @@ export function createStore, C extend const id = config.id || idGenerator.get('UNNAMED_STORE'); - const proxyObj = createProxy(config.state, id, !config.options?.isReduxAdapter); + const listener = { + current: listener => {}, + }; + + const proxyObj = createProxy(config.state, !config.options?.isReduxAdapter, listener); proxyObj.$pending = false; @@ -76,16 +81,28 @@ export function createStore, C extend $c: $c as ComputedValues, $queue: $queue as QueuedStoreActions, $config: config, + $listeners: [ + change => { + devtools.emit(STATE_CHANGE, { + store: storeObj, + change, + }); + }, + ], $subscribe: listener => { devtools.emit(SUBSCRIBED, { store: storeObj, listener }); - proxyObj.addListener(listener); + storeObj.$listeners.push(listener); }, $unsubscribe: listener => { - devtools.emit(UNSUBSCRIBED, storeObj); - proxyObj.removeListener(listener); + devtools.emit(UNSUBSCRIBED, { store: storeObj }); + storeObj.$listeners = storeObj.$listeners.filter(item => item != listener); }, } as unknown as StoreObj; + listener.current = (...args) => { + storeObj.$listeners.forEach(listener => listener(...args)); + }; + const plannedActions: PlannedAction>[] = []; // 包装actions @@ -104,7 +121,11 @@ export function createStore, C extend }); return new Promise(resolve => { if (!proxyObj.$pending) { - proxyObj.$pending = true; + proxyObj.$pending = Date.now(); + devtools.emit(QUEUE_PENDING, { + store: storeObj, + startedAt: proxyObj.$pending, + }); const result = config.actions![action].bind(storeObj, proxyObj)(...payload); @@ -192,20 +213,22 @@ export function createStore, C extend store: storeObj, }); - proxyObj.addListener(change => { - devtools.emit(STATE_CHANGE, { - store: storeObj, - change, - }); - }); - return createGetStore(storeObj); } // 通过该方法执行store.$queue中的action function tryNextAction(storeObj, proxyObj, config, plannedActions) { if (!plannedActions.length) { - proxyObj.$pending = false; + if (proxyObj.$pending) { + const timestamp = Date.now(); + const duration = timestamp - proxyObj.$pending; + proxyObj.$pending = false; + devtools.emit(QUEUE_FINISHED, { + store: storeObj, + endedAt: timestamp, + duration, + }); + } return; } diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts index eec4a826..d617037c 100644 --- a/libs/horizon/src/horizonx/types.d.ts +++ b/libs/horizon/src/horizonx/types.d.ts @@ -61,6 +61,7 @@ export type StoreObj, C extends UserC $a: StoreActions; $c: UserComputedValues; $queue: QueuedStoreActions; + $listeners; $subscribe: (listener: (mutation) => void) => void; $unsubscribe: (listener: (mutation) => void) => void; } & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; diff --git a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx similarity index 98% rename from scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx index 5f4afe65..d83031df 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx +++ b/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx @@ -73,13 +73,13 @@ describe('测试store中的Map', () => { function Parent(props) { const userStore = useUserStore(); - const addOnePerson = function() { + const addOnePerson = function () { userStore.addOnePerson(newPerson); }; - const delOnePerson = function() { + const delOnePerson = function () { userStore.delOnePerson(newPerson); }; - const clearPersons = function() { + const clearPersons = function () { userStore.clearPersons(); }; diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/cloneDeep.test.js b/scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/cloneDeep.test.js rename to scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.js b/scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/reset.js rename to scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/store.ts b/scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/store.ts rename to scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js b/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js similarity index 87% rename from scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js rename to scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js index 7d1f786f..be07b4c4 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js +++ b/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js @@ -64,7 +64,6 @@ describe('Mutation resolve', () => { it('should resolve mutation same type types, same object', () => { const mutation = resolveMutation({ a: 1, b: 2 }, { a: 1, b: 2 }); - console.log(mutation); expect(mutation.mutation).toBe(false); }); @@ -78,3 +77,17 @@ describe('Mutation resolve', () => { expect(mutation.attributes.c.to).toBe(2); }); }); + +describe('Mutation collections', () => { + it('should resolve mutation of two sets', () => { + const values = [{ a: 1 }, { b: 2 }, { c: 3 }]; + + const source = new Set([values[0], values[1], values[2]]); + + const target = new Set([values[0], values[1]]); + + const mutation = resolveMutation(source, target); + + expect(mutation.mutation).toBe(true); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx similarity index 98% rename from scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx index 8d685539..c4307f26 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx +++ b/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx @@ -17,7 +17,7 @@ import { createStore } from '@cloudsop/horizon/src/horizonx/store/StoreHandler'; import { watch } from '@cloudsop/horizon/src/horizonx/proxy/watch'; describe('watch', () => { - it('shouhld watch promitive state variable', async () => { + it('shouhld watch primitive state variable', async () => { const useStore = createStore({ state: { variable: 'x', diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx b/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx rename to scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx b/scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx rename to scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx b/scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx rename to scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx diff --git a/scripts/__tests__/HorizonXText/adapters/connectTest.tsx b/scripts/__tests__/HorizonXTest/adapters/connectTest.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/connectTest.tsx rename to scripts/__tests__/HorizonXTest/adapters/connectTest.tsx diff --git a/scripts/__tests__/HorizonXText/class/ClassException.test.tsx b/scripts/__tests__/HorizonXTest/class/ClassException.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/class/ClassException.test.tsx rename to scripts/__tests__/HorizonXTest/class/ClassException.test.tsx diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx b/scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx rename to scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx diff --git a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx b/scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx rename to scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx diff --git a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx b/scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx rename to scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx diff --git a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx b/scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx rename to scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx diff --git a/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx b/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx new file mode 100644 index 00000000..6fff3531 --- /dev/null +++ b/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx @@ -0,0 +1,155 @@ +import { createStore, useStore } from '@cloudsop/horizon/src/horizonx/store/StoreHandler'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +describe('Using deep variables', () => { + it('should listen to object variable change', () => { + let counter = 0; + const useTestStore = createStore({ + state: { a: { b: { c: 1 } } }, + }); + const testStore = useTestStore(); + testStore.$subscribe(() => { + counter++; + }); + + testStore.a.b.c = 0; + + expect(counter).toBe(1); + }); + + it('should listen to deep variable change', () => { + let counter = 0; + const useTestStore = createStore({ + state: { color: [{ a: 1 }, 255, 255] }, + }); + const testStore = useTestStore(); + testStore.$subscribe(() => { + counter++; + }); + + for (let i = 0; i < 5; i++) { + testStore.color[0].a = i; + } + testStore.color = 'x'; + + expect(counter).toBe(6); + }); + + it('should use set', () => { + const useTestStore = createStore({ + state: { data: new Set() }, + }); + const testStore = useTestStore(); + + const a = { a: true }; + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + + testStore.data.add(a); + testStore.data.add(a); + testStore.data.delete(a); + + expect(testStore.data.has(a)).toBe(false); + + testStore.data.add(a); + + const values = Array.from(testStore.data.values()); + expect(values.length).toBe(1); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + values.forEach(val => { + val.a = !val.a; + }); + + expect(testStore.data.has(a)).toBe(true); + + expect(counter).toBe(1); + }); + + it('should use map', () => { + const useTestStore = createStore({ + state: { data: new Map() }, + }); + const testStore = useTestStore(); + + const data = { key: { a: 1 }, value: { b: 2 } }; + + testStore.data.set(data.key, data.value); + + const key = Array.from(testStore.data.keys())[0]; + + expect(testStore.data.has(key)).toBe(true); + + testStore.data.set(data.key, data.value); + testStore.data.set(data.key, data.value); + testStore.data.delete(key); + + expect(testStore.data.get(key)).toBe(); + + testStore.data.set(data.key, data.value); + + const entries = Array.from(testStore.data.entries()); + expect(entries.length).toBe(1); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + entries.forEach(([key, value]) => { + key.a++; + value.b++; + }); + + expect(counter).toBe(2); + }); + + it('should use weakSet', () => { + const useTestStore = createStore({ + state: { data: new WeakSet() }, + }); + const testStore = useTestStore(); + + const a = { a: true }; + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + + testStore.data.add(a); + testStore.data.add(a); + testStore.data.delete(a); + + expect(testStore.data.has(a)).toBe(false); + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + }); + + it('should use weakMap', () => { + const useTestStore = createStore({ + state: { data: new WeakMap() }, + }); + const testStore = useTestStore(); + + const data = { key: { a: 1 }, value: { b: 2 } }; + + testStore.data.set(data.key, data.value); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + testStore.data.get(data.key).b++; + + expect(counter).toBe(1); + }); +}); diff --git a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx b/scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx rename to scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx b/scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx rename to scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx