From d27ee983fbe97535738577e743eb50ac55c538e0 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 15 Jul 2022 21:14:22 +0800 Subject: [PATCH 01/19] Match-id-ee12b2c587d5ebf5b2b49cd40df333ed3386118d --- jest.config.js | 3 +- libs/horizon/src/horizonx/adapters/redux.ts | 35 ++- .../src/horizonx/adapters/reduxReact.ts | 37 +-- .../src/horizonx/adapters/reduxThunk.ts | 3 +- .../src/horizonx/proxy/HooklessObserver.ts | 2 +- libs/horizon/src/horizonx/proxy/Observer.ts | 21 +- .../src/horizonx/proxy/ProxyHandler.ts | 2 +- .../src/horizonx/store/StoreHandler.ts | 163 ++++++---- libs/horizon/src/horizonx/types.d.ts | 81 ----- package.json | 6 +- .../StateManager/StateArray.test.js | 201 ------------- .../StateManager/StateArray.test.tsx | 208 +++++++++++++ .../{StateMap.test.js => StateMap.test.tsx} | 152 +++++----- ...eMixType.test.js => StateMixType.test.tsx} | 37 ++- .../{StateSet.test.js => StateSet.test.tsx} | 151 +++++----- ...eWeakMap.test.js => StateWeakMap.test.tsx} | 80 ++--- ...eWeakSet.test.js => StateWeakSet.test.tsx} | 73 +++-- .../{async.test.js => async.test.tsx} | 27 +- .../StoreFunctionality/basicAccess.test.js | 63 ---- .../StoreFunctionality/basicAccess.test.tsx | 110 +++++++ ...arAccess.test.js => dollarAccess.test.tsx} | 20 +- ...otherCases.test.js => otherCases.test.tsx} | 36 +-- .../{reset.test.js => reset.js} | 42 ++- .../StoreFunctionality/{store.js => store.ts} | 0 ...xAdapter.test.js => ReduxAdapter.test.tsx} | 153 +++++----- .../adapters/ReduxAdapterThunk.test.js | 34 --- .../adapters/ReduxAdapterThunk.test.tsx | 33 +++ ...ter.test.js => ReduxReactAdapter.test.tsx} | 278 +++++++----------- ...eption.test.js => ClassException.test.tsx} | 38 +-- .../class/ClassStateArray.test.js | 220 -------------- .../class/ClassStateArray.test.tsx | 231 +++++++++++++++ ...tateMap.test.js => ClassStateMap.test.tsx} | 165 ++++++----- ...Clear.test.js => ClassVNodeClear.test.tsx} | 63 ++-- ...ar.test.js => FunctionVNodeClear.test.tsx} | 65 ++-- .../HorizonXText/edgeCases/proxy.test.js | 21 -- .../HorizonXText/edgeCases/proxy.test.tsx | 48 +++ scripts/__tests__/jest/logUtils.js | 26 ++ scripts/rollup/rollup.config.js | 2 + 38 files changed, 1538 insertions(+), 1392 deletions(-) delete mode 100644 libs/horizon/src/horizonx/types.d.ts delete mode 100644 scripts/__tests__/HorizonXText/StateManager/StateArray.test.js create mode 100644 scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx rename scripts/__tests__/HorizonXText/StateManager/{StateMap.test.js => StateMap.test.tsx} (60%) rename scripts/__tests__/HorizonXText/StateManager/{StateMixType.test.js => StateMixType.test.tsx} (77%) rename scripts/__tests__/HorizonXText/StateManager/{StateSet.test.js => StateSet.test.tsx} (58%) rename scripts/__tests__/HorizonXText/StateManager/{StateWeakMap.test.js => StateWeakMap.test.tsx} (61%) rename scripts/__tests__/HorizonXText/StateManager/{StateWeakSet.test.js => StateWeakSet.test.tsx} (59%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{async.test.js => async.test.tsx} (80%) delete mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx rename scripts/__tests__/HorizonXText/StoreFunctionality/{dollarAccess.test.js => dollarAccess.test.tsx} (64%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{otherCases.test.js => otherCases.test.tsx} (71%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{reset.test.js => reset.js} (65%) rename scripts/__tests__/HorizonXText/StoreFunctionality/{store.js => store.ts} (100%) rename scripts/__tests__/HorizonXText/adapters/{ReduxAdapter.test.js => ReduxAdapter.test.tsx} (59%) delete mode 100644 scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js create mode 100644 scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx rename scripts/__tests__/HorizonXText/adapters/{ReduxReactAdapter.test.js => ReduxReactAdapter.test.tsx} (50%) rename scripts/__tests__/HorizonXText/class/{ClassException.test.js => ClassException.test.tsx} (54%) delete mode 100644 scripts/__tests__/HorizonXText/class/ClassStateArray.test.js create mode 100644 scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx rename scripts/__tests__/HorizonXText/class/{ClassStateMap.test.js => ClassStateMap.test.tsx} (58%) rename scripts/__tests__/HorizonXText/clear/{ClassVNodeClear.test.js => ClassVNodeClear.test.tsx} (62%) rename scripts/__tests__/HorizonXText/clear/{FunctionVNodeClear.test.js => FunctionVNodeClear.test.tsx} (59%) delete mode 100644 scripts/__tests__/HorizonXText/edgeCases/proxy.test.js create mode 100644 scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx create mode 100644 scripts/__tests__/jest/logUtils.js diff --git a/jest.config.js b/jest.config.js index 665708c9..dde828dd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -138,7 +138,8 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ - '/scripts/__tests__/**/*.test.js' + '/scripts/__tests__/**/*.test.js', + '/scripts/__tests__/**/*.test.tsx' ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index 293700b6..fb19cf9a 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -1,14 +1,38 @@ import { createStore as createStoreX } from '../store/StoreHandler'; -import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; +import { ReduxStoreHandler } from '../store/StoreHandler'; export { thunk } from './reduxThunk'; -export { Provider, useSelector, useStore, useDispatch, connect, createSelectorHook, createDispatchHook } from './reduxReact'; +export { + Provider, + useSelector, + useStore, + useDispatch, + connect, + createSelectorHook, + createDispatchHook, +} from './reduxReact'; + +export type ReduxAction = { + type: string; + [key: string]: any; +}; + +export type ReduxMiddleware = ( + store: ReduxStoreHandler, + extraArgument?: any +) => ( + next: (action: ReduxAction) => any +) => ( + action: + | ReduxAction + | ((dispatch: (action: ReduxAction) => void, store: ReduxStoreHandler, extraArgument?: any) => any) +) => ReduxStoreHandler; type Reducer = (state: any, action: ReduxAction) => any; -export function createStore(reducer: Reducer, preloadedState: any, enhancers): ReduxStoreHandler { +export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', state: { stateWrapper: preloadedState }, @@ -35,7 +59,7 @@ export function createStore(reducer: Reducer, preloadedState: any, enhancers): R const result = { reducer, getState: function() { - return store.$state.stateWrapper; + return store.$s.stateWrapper; }, subscribe: listener => { store.$subscribe(listener); @@ -48,7 +72,7 @@ export function createStore(reducer: Reducer, preloadedState: any, enhancers): R reducer = newReducer; }, _horizonXstore: store, - dispatch: store.$actions.dispatch, + dispatch: store.$a.dispatch, }; enhancers && enhancers(result); @@ -117,7 +141,6 @@ export function compose(middlewares: ReduxMiddleware[]) { }; } - // HorizonX batches updates by default, this function is only for backwards compatibility export function batch(fn: () => void) { fn(); diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index da99ab52..8e205eb7 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -3,9 +3,10 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import { BoundActionCreator } from './redux'; -import { ReduxAction, ReduxStoreHandler } from '../types'; +import { ReduxAction } from './redux'; +import { ReduxStoreHandler } from '../store/StoreHandler' -const DefaultContext = createContext(); +const DefaultContext = createContext(null); type Context = typeof DefaultContext; export function Provider({ @@ -27,8 +28,8 @@ export function createStoreHook(context: Context) { }; } -export function createSelectorHook(context: Context): (selector: (any) => any) => any { - const store = createStoreHook(context)(); +export function createSelectorHook(context: Context): (selector?: (any) => any) => any { + const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; return function(selector = state => state) { const [b, fr] = useState(false); @@ -37,22 +38,18 @@ export function createSelectorHook(context: Context): (selector: (any) => any) = }; useEffect(() => { - const unsubscribe = store.subscribe(listener); - - return () => { - unsubscribe(listener); - }; + return store.subscribe(listener); }); return selector(store.getState()); }; } -export function createDispatchHook(context: Context): BoundActionCreator { - const store = createStoreHook(context)(); +export function createDispatchHook(context: Context): ()=>BoundActionCreator { + const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; return function() { return action => { - this.dispatch(action); + store.dispatch(action); }; }.bind(store); } @@ -104,26 +101,32 @@ export function connect( } return Component => { - const useStore = createStoreHook(options.context || DefaultContext); + const useStore = createStoreHook(options?.context || DefaultContext); function Wrapper(props) { const [f, forceReload] = useState(true); - const store = useStore(); + const store = (useStore() as unknown) as ReduxStoreHandler; useEffect(() => { const unsubscribe = store.subscribe(() => forceReload(!f)); () => { - unsubscribe(() => forceReload(!f)); + unsubscribe(); }; }); const previous = useRef({ state: {}, - }); + mappedState: {}, + }) as { + current: { + state: {}; + mappedState: {}; + }; + }; let mappedState; - if (options.areStatesEqual) { + if (options?.areStatesEqual) { if (options.areStatesEqual(previous.current.state, store.getState())) { mappedState = previous.current.mappedState; } else { diff --git a/libs/horizon/src/horizonx/adapters/reduxThunk.ts b/libs/horizon/src/horizonx/adapters/reduxThunk.ts index 850f1b1c..28ce23e0 100644 --- a/libs/horizon/src/horizonx/adapters/reduxThunk.ts +++ b/libs/horizon/src/horizonx/adapters/reduxThunk.ts @@ -1,4 +1,5 @@ -import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; +import { ReduxAction, ReduxMiddleware } from './redux'; +import { ReduxStoreHandler } from '../store/StoreHandler'; function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { return (store: ReduxStoreHandler) => (next: (action: ReduxAction) => any) => ( diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index 9ea8937f..1de98c5f 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -1,6 +1,6 @@ // TODO: implement vNode type -import {IObserver} from '../types'; +import {IObserver} from './Observer'; /** * 一个对象(对象、数组、集合)对应一个Observer diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index be24d1c2..a6348841 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -6,7 +6,24 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { VNode } from '../../renderer/vnode/VNode'; -import { IObserver } from '../types'; +export interface IObserver { + + useProp: (key: string) => void; + + addListener: (listener: () => void) => void; + + removeListener: (listener: () => void) => void; + + setProp: (key: string) => void; + + triggerChangeListeners: () => void; + + triggerUpdate: (vNode: any) => void; + + allChange: () => void; + + clearByVNode: (vNode: any) => void; +} export class Observer implements IObserver { @@ -85,7 +102,7 @@ export class Observer implements IObserver { } } - clearByVNode(vNode: Vnode): void { + clearByVNode(vNode: VNode): void { const keys = this.vNodeKeys.get(vNode); if (keys) { keys.forEach((key: any) => { diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index f2051b9d..b3519729 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -4,7 +4,7 @@ import {HooklessObserver} from './HooklessObserver'; import {isArray, isCollection, isObject} from '../CommonUtils'; import {createArrayProxy} from './handlers/ArrayProxyHandler'; import {createCollectionProxy} from './handlers/CollectionProxyHandler'; -import { IObserver } from '../types'; +import { IObserver } from './Observer'; const OBSERVER_KEY = Symbol('_horizonObserver'); diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 9b746c96..ffc67aa8 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -3,31 +3,88 @@ import { useEffect, useRef } from '../../renderer/hooks/HookExternal'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { createProxy } from '../proxy/ProxyHandler'; import readonlyProxy from '../proxy/readonlyProxy'; -import { StoreHandler, StoreConfig, UserActions, UserComputedValues, StoreActions, ComputedValues, ActionFunction, Action, QueuedStoreActions } from '../types'; import { Observer } from '../proxy/Observer'; import { FunctionComponent, ClassComponent } from '../Constants'; +import { VNode } from '../../renderer/Types'; -const storeMap = new Map>(); +const storeMap = new Map>(); function isPromise(obj: any): boolean { return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; } -type PlannedAction>={ - action:string, - payload: any[], - resolve: ReturnType -} +type StoreConfig, C extends UserComputedValues> = { + state?: S; + options?: { suppressHooks?: boolean }; + actions?: A; + id?: string; + computed?: C; +}; -export function createStore,C extends UserComputedValues>(config: StoreConfig): () => StoreHandler { +export type ReduxStoreHandler = { + reducer: (state: any, action: { type: string }) => any; + dispatch: (action: { type: string }) => void; + getState: () => any; + subscribe: (listener: () => void) => () => void; + replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; + _horizonXstore: StoreHandler; +}; + +type StoreHandler, C extends UserComputedValues> = { + $subscribe: (listener: () => void) => void; + $unsubscribe: (listener: () => void) => void; + $s: S; + $config: StoreConfig; + $queue: QueuedStoreActions; + $a: StoreActions; + $c: UserComputedValues; + reduxHandler?: ReduxStoreHandler; +} & { [K in keyof S]: S[K] } & + { [K in keyof A]: Action } & + { [K in keyof C]: ReturnType }; + +type PlannedAction> = { + action: string; + payload: any[]; + resolve: ReturnType; +}; +type RemoveFirstFromTuple = T['length'] extends 0 + ? [] + : ((...b: T) => void) extends (a, ...b: infer I) => void + ? I + : []; + +type UserActions = { [K: string]: ActionFunction }; +type UserComputedValues = { [K: string]: ComputedFunction }; + +type ActionFunction = (this: StoreHandler, state: S, ...args: any[]) => any; +type ComputedFunction = (state: S) => any; +type Action, S extends object> = ( + this: StoreHandler, + ...args: RemoveFirstFromTuple> +) => ReturnType; +type AsyncAction, S extends object> = ( + this: StoreHandler, + ...args: RemoveFirstFromTuple> +) => Promise>; + +type StoreActions> = { [K in keyof A]: Action }; +type QueuedStoreActions> = { [K in keyof A]: AsyncAction }; +type ComputedValues> = { [K in keyof C]: ReturnType }; +type PostponedAction = (state: object, ...args: any[]) => Promise; +type PostponedActions = { [key: string]: PostponedAction }; + +export function createStore, C extends UserComputedValues>( + config: StoreConfig +): () => StoreHandler { //create a local shalow copy to ensure consistency (if user would change the config object after store creation) config = { - id:config.id, + id: config.id, options: config.options, state: config.state, - actions: config.actions ? {...config.actions}:undefined, - computed: config.computed ? {...config.computed}:undefined - } + actions: config.actions ? { ...config.actions } : undefined, + computed: config.computed ? { ...config.computed } : undefined, + }; // 校验 if (Object.prototype.toString.call(config) !== '[object Object]') { @@ -35,30 +92,30 @@ export function createStore,C extends } const proxyObj = createProxy(config.state, !config.options?.suppressHooks); - + proxyObj.$pending = false; - - const $subscribe = (listener) => { + + const $subscribe = listener => { proxyObj.addListener(listener); }; - - const $unsubscribe = (listener) => { + + const $unsubscribe = listener => { proxyObj.removeListener(listener); }; - const plannedActions:PlannedAction>[] = []; - const $actions:Partial>={} - const $queue:Partial> = {}; - const $computed:Partial>={} - const handler = { + const plannedActions: PlannedAction>[] = []; + const $a: Partial> = {}; + const $queue: Partial> = {}; + const $c: Partial> = {}; + const handler = ({ $subscribe, $unsubscribe, - $actions:$actions as StoreActions, - $state:proxyObj, - $computed: $computed as ComputedValues, - $config:config, - $queue: $queue as QueuedStoreActions, - } as StoreHandler; + $a: $a as StoreActions, + $s: proxyObj, + $c: $c as ComputedValues, + $config: config, + $queue: $queue as QueuedStoreActions, + } as unknown) as StoreHandler; function tryNextAction() { if (!plannedActions.length) { @@ -67,7 +124,9 @@ export function createStore,C extends } const nextAction = plannedActions.shift()!; - const result = config.actions ? config.actions[nextAction.action].bind(self, proxyObj)(...nextAction.payload) : undefined; + const result = config.actions + ? config.actions[nextAction.action].bind(handler, proxyObj)(...nextAction.payload) + : undefined; if (isPromise(result)) { result.then(value => { @@ -81,16 +140,16 @@ export function createStore,C extends } // 包装actions - if(config.actions){ + if (config.actions) { Object.keys(config.actions).forEach(action => { ($queue as any)[action] = (...payload) => { - return new Promise((resolve) => { + return new Promise(resolve => { if (!proxyObj.$pending) { proxyObj.$pending = true; - const result = config.actions![action].bind(self, proxyObj)(...payload); - + const result = config.actions![action].bind(handler, proxyObj)(...payload); + if (isPromise(result)) { - result.then((value) => { + result.then(value => { resolve(value); tryNextAction(); }); @@ -102,40 +161,42 @@ export function createStore,C extends plannedActions.push({ action, payload, - resolve + resolve, }); } }); }; - - ($actions as any)[action] = function Wrapped(...payload) { - return config.actions![action].bind(self, proxyObj)(...payload); + + ($a as any)[action] = function Wrapped(...payload) { + return config.actions![action].bind(handler, proxyObj)(...payload); }; - // direct store access + // direct store access Object.defineProperty(handler, action, { writable: false, - value: $actions[action] + value: (...payload) => { + return config.actions![action].bind(handler, proxyObj)(...payload); + }, }); }); } - if (config.computed) { - Object.keys(config.computed).forEach((key) => { - ($computed as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj)); + if (config.computed) { + Object.keys(config.computed).forEach(key => { + ($c as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj)); - // direct store access + // direct store access Object.defineProperty(handler, key, { - get: $computed[key] as ()=>any + get: $c[key] as () => any, }); }); } // direct state access - if(config.state){ + if (config.state) { Object.keys(config.state).forEach(key => { Object.defineProperty(handler, key, { - get: () => proxyObj[key] + get: () => proxyObj[key], }); }); } @@ -172,7 +233,7 @@ function hookStore() { if (processingVNode.tag === FunctionComponent) { // from FunctionComponent - const vNodeRef = useRef(null); + const vNodeRef = (useRef(null) as unknown) as { current: VNode }; vNodeRef.current = processingVNode; useEffect(() => { @@ -211,9 +272,9 @@ export function useStore, C extends U if (storeObj && !storeObj.$config.options?.suppressHooks) hookStore(); - return storeObj as StoreHandler; + return storeObj as StoreHandler; } -export function clearStore(id:string):void { +export function clearStore(id: string): void { storeMap.delete(id); -} \ No newline at end of file +} diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts deleted file mode 100644 index 3fed3afe..00000000 --- a/libs/horizon/src/horizonx/types.d.ts +++ /dev/null @@ -1,81 +0,0 @@ -export interface IObserver { - - useProp: (key: string) => void; - - addListener: (listener: () => void) => void; - - removeListener: (listener: () => void) => void; - - setProp: (key: string) => void; - - triggerChangeListeners: () => void; - - triggerUpdate: (vNode: any) => void; - - allChange: () => void; - - clearByVNode: (vNode: any) => void; -} - -type RemoveFirstFromTuple = - T['length'] extends 0 ? [] : - (((...b: T) => void) extends (a, ...b: infer I) => void ? I : []) - - -type UserActions = { [K:string]: ActionFunction }; -type UserComputedValues = { [K:string]: ComputedFunction }; - -type ActionFunction = (state: S, ...args: any[]) => any; -type ComputedFunction = (state: S) => any; -type Action> = (...args:RemoveFirstFromTuple>)=>ReturnType -type AsyncAction> = (...args:RemoveFirstFromTuple>)=>Promise> - -type StoreActions> = { [K in keyof A]: Action }; -type QueuedStoreActions> = { [K in keyof A]: AsyncAction }; -type ComputedValues> = { [K in keyof C]: ReturnType }; -type PostponedAction = (state: object, ...args: any[]) => Promise; -type PostponedActions = { [key:string]: PostponedAction } - -export type StoreHandler,C extends UserComputedValues> = - {$subscribe: ((listener: () => void) => void), - $unsubscribe: ((listener: () => void) => void), - $state: S, - $config: StoreConfig, - $queue: QueuedStoreActions, - $actions: StoreActions, - $computed: ComputedValues, - reduxHandler?:ReduxStoreHandler} - & - {[K in keyof S]: S[K]} - & - {[K in keyof A]: Action} - & - {[K in keyof C]: ReturnType} - -export type StoreConfig,C extends UserComputedValues> = { - state?: S, - options?:{suppressHooks?: boolean}, - actions?: A, - id?: string, - computed?: C -} - -type ReduxStoreHandler = { - reducer:(state:any,action:{type:string})=>any, - dispatch:(action:{type:string})=>void, - getState:()=>any, - subscribe:(listener:()=>void)=>((listener:()=>void)=>void) - replaceReducer: (reducer: (state:any,action:{type:string})=>any)=>void - _horizonXstore: StoreHandler -} - -type ReduxAction = { - type:string -} - -type ReduxMiddleware = (store:ReduxStoreHandler, extraArgument?:any) => - (next:((action:ReduxAction)=>any)) => - (action:( - ReduxAction| - ((dispatch:(action:ReduxAction)=>void,store:ReduxStoreHandler,extraArgument?:any)=>any) - )) => ReduxStoreHandler \ No newline at end of file diff --git a/package.json b/package.json index c31b27cf..7f37e8cd 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ ], "scripts": { "lint": "eslint . --ext .ts", - "build": " rollup --config ./scripts/rollup/rollup.config.js", - "build:watch": " rollup --watch --config ./scripts/rollup/rollup.config.js", + "build": "rollup --config ./scripts/rollup/rollup.config.js", + "build:watch": "rollup --watch --config ./scripts/rollup/rollup.config.js", "build-3rdLib": "node ./scripts/gen3rdLib.js", "build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev", "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon", + "build-types": "tsc -p ./libs/horizon/index.ts --emitDeclarationOnly --declaration --declarationDir ./build/horizon/@types --skipLibCheck", "debug-test": "yarn test --debug", "test": "jest --config=jest.config.js", "watch-test": "yarn test --watch --dev" @@ -92,6 +93,7 @@ "regenerator-runtime": "^0.13.9", "rimraf": "^3.0.0", "rollup": "^2.75.5", + "rollup-plugin-execute": "^1.1.1", "rollup-plugin-terser": "^7.0.2", "typescript": "^3.9.7" }, diff --git a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js deleted file mode 100644 index 826d6dda..00000000 --- a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.js +++ /dev/null @@ -1,201 +0,0 @@ -import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; - -describe('测试store中的Array', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; - beforeEach(() => { - // 创建一个 DOM 元素作为渲染目标 - container = document.createElement('div'); - document.body.appendChild(container); - - const persons = [ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]; - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.push(person); - }, - delOnePerson: state => { - state.persons.pop(); - }, - clearPersons: state => { - state.persons = null; - }, - }, - }); - }); - - afterEach(() => { - // 退出时进行清理 - unmountComponentAtNode(container); - container.remove(); - container = null; - - clearStore('user'); - }); - - const newPerson = { name: 'p3', age: 3 }; - - function Parent(props) { - const userStore = useStore('user'); - const addOnePerson = function() { - userStore.addOnePerson(newPerson); - }; - const delOnePerson = function() { - userStore.delOnePerson(); - }; - return ( -
- - -
{props.children}
-
- ); - } - - it('测试Array方法: push()、pop()', () => { - function Child(props) { - const userStore = useStore('user'); - - return ( -
- -
- ); - } - - Horizon.render(, container); - - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - // 在Array中增加一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'addBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); - - // 在Array中删除一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'delBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - }); - - it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { - let globalStore = null; - - function Child(props) { - const userStore = useStore('user'); - globalStore = userStore; - - const nameList = []; - const entries = userStore.$state.persons?.entries(); - if (entries) { - for (const entry of entries) { - nameList.push(entry[1].name); - } - } - - return ( -
- -
- ); - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); - - it('测试Array方法: forEach()', () => { - let globalStore = null; - - function Child(props) { - const userStore = useStore('user'); - globalStore = userStore; - - const nameList = []; - userStore.$state.persons?.forEach(per => { - nameList.push(per.name); - }); - - return ( -
- -
- ); - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); -}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx new file mode 100644 index 00000000..fbe1de0b --- /dev/null +++ b/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx @@ -0,0 +1,208 @@ +//@ts-ignore +import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; +import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ], + }, + actions: { + addOnePerson: (state, person) => { + state.persons.push(person); + }, + delOnePerson: state => { + state.persons.pop(); + }, + clearPersons: state => { + state.persons = []; + }, + reset: state => { + state.persons = [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]; + }, + }, +}); + +describe('测试store中的Array', () => { + const { unmountComponentAtNode } = Horizon; + let container: HTMLElement | null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + useUserStore().reset(); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + function Parent(props) { + const userStore = useUserStore(); + const addOnePerson = function() { + userStore.addOnePerson(newPerson); + }; + const delOnePerson = function() { + userStore.delOnePerson(); + }; + return ( +
+ + +
{props.children}
+
+ ); + } + + it('测试Array方法: push()、pop()', () => { + function Child(props) { + const userStore = useUserStore(); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = useUserStore(); + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + const entries = userStore.$s.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + //@ts-ignore TODO:why is this argument here? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = useUserStore(); + function Child(props) { + const userStore = useUserStore(); + + const nameList: string[] = []; + userStore.$s.persons?.forEach(per => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + //@ts-ignore TODO: why is this argument here? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx similarity index 60% rename from scripts/__tests__/HorizonXText/StateManager/StateMap.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx index f7eaed9f..e5d1b141 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx @@ -1,45 +1,55 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Map([ + ['p1', 1], + ['p2', 2], + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person.name, person.age); + }, + delOnePerson: (state, person) => { + state.persons.delete(person.name); + }, + clearPersons: state => { + state.persons.clear(); + }, + reset: state => { + state.persons = new Map([ + ['p1', 1], + ['p2', 2], + ]); + }, + }, +}); describe('测试store中的Map', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new Map([ - ['p1', 1], - ['p2', 2], - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.set(person.name, person.age); - }, - delOnePerson: (state, person) => { - state.persons.delete(person.name); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,7 +57,7 @@ describe('测试store中的Map', () => { const newPerson = { name: 'p3', age: 3 }; function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -76,43 +86,43 @@ describe('测试store中的Map', () => { it('测试Map方法: set()、delete()、clear()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); }); it('测试Map方法: keys()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const keys = userStore.$state.persons.keys(); + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); for (const key of keys) { nameList.push(key); } @@ -126,32 +136,32 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: values()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const ageList = []; - const values = userStore.$state.persons.values(); + const ageList: number[] = []; + const values = userStore.$s.persons.values(); for (const val of values) { ageList.push(val); } @@ -165,32 +175,32 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: '); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: '); }); it('测试Map方法: entries()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const entries = userStore.$state.persons.entries(); + const nameList: string[] = []; + const entries = userStore.$s.persons.entries(); for (const entry of entries) { nameList.push(entry[0]); } @@ -204,32 +214,32 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: forEach()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - userStore.$state.persons.forEach((val, key) => { + const nameList: string[] = []; + userStore.$s.persons.forEach((val, key) => { nameList.push(key); }); @@ -242,53 +252,53 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); }); it('测试Map方法: for of()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - for (const per of userStore.$state.persons) { + const nameList: string[] = []; + for (const per of userStore.$s.persons) { nameList.push(per[0]); } @@ -301,23 +311,23 @@ describe('测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx similarity index 77% rename from scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx index 870d7d26..bb38773d 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx @@ -1,10 +1,12 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; describe('测试store中的混合类型变化', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); @@ -42,8 +44,9 @@ describe('测试store中的混合类型变化', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + (container as HTMLElement).remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -68,7 +71,7 @@ describe('测试store中的混合类型变化', () => { function Child(props) { const userStore = useStore('user'); - const days = userStore.$state.persons + const days = userStore.persons .values() .next() .value.love.get('lanqiu').days; @@ -82,11 +85,11 @@ describe('测试store中的混合类型变化', () => { Horizon.render(, container); - expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5'); + expect(container?.querySelector('#dayList')?.innerHTML).toBe('love: 1 3 5'); Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5 7'); + expect(container?.querySelector('#dayList')?.innerHTML).toBe('love: 1 3 5 7'); }); it('属性是个class实例', () => { @@ -103,7 +106,6 @@ describe('测试store中的混合类型变化', () => { setName(name) { this.name = name; } - getName() { return this.name; } @@ -111,7 +113,6 @@ describe('测试store中的混合类型变化', () => { setAge(age) { this.age = age; } - getAge() { return this.age; } @@ -119,7 +120,6 @@ describe('测试store中的混合类型变化', () => { addLove(lv) { this.loves.add(lv); } - getLoves() { return this.loves; } @@ -127,14 +127,19 @@ describe('测试store中的混合类型变化', () => { let globalPerson; let globalStore; - function Child(props) { const userStore = useStore('user'); globalStore = userStore; - const nameList = []; - const valIterator = userStore.$state.persons.values(); - let per = valIterator.next(); + const nameList: string[] = []; + const valIterator = userStore.persons.values(); + let per = valIterator.next() as { + value: { + name: string; + getName: () => string; + }; + done: boolean; + }; while (!per.done) { nameList.push(per.value.name ?? per.value.getName()); globalPerson = per.value; @@ -150,15 +155,15 @@ describe('测试store中的混合类型变化', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2'); // 动态增加一个Person实例 - globalStore.$state.persons.add(new Person('ClassPerson', 5)); + globalStore.$s.persons.add(new Person('ClassPerson', 5)); - expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2 ClassPerson'); globalPerson.setName('ClassPerson1'); - expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson1'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('p1 p2 ClassPerson1'); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx similarity index 58% rename from scripts/__tests__/HorizonXText/StateManager/StateSet.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx index 4b6bdc7b..a1a245f0 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx @@ -1,53 +1,62 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Set([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.add(person); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons.clear(); + }, + reset: state => { + state.persons = new Set([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]); + }, + }, +}); describe('测试store中的Set', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new Set([ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.add(person); - }, - delOnePerson: (state, person) => { - state.persons.delete(person); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); const newPerson = { name: 'p3', age: 3 }; - function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -76,17 +85,17 @@ describe('测试store中的Set', () => { it('测试Set方法: add()、delete()、clear()', () => { function Child(props) { - const userStore = useStore('user'); - const personArr = Array.from(userStore.$state.persons); - const nameList = []; - const keys = userStore.$state.persons.keys(); + const userStore = useUserStore(); + const personArr = Array.from(userStore.$s.persons); + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); for (const key of keys) { nameList.push(key.name); } return (
- +
); @@ -94,35 +103,35 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); - expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#lastAge')?.innerHTML).toBe('last person age: 2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); - expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 0'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); + expect(container?.querySelector('#lastAge')?.innerHTML).toBe('last person age: 0'); }); it('测试Set方法: keys()、values()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const keys = userStore.$state.persons.keys(); - // const keys = userStore.$state.persons.values(); + const nameList: string[] = []; + const keys = userStore.$s.persons.keys(); + // const keys = userStore.$s.persons.values(); for (const key of keys) { nameList.push(key.name); } @@ -136,32 +145,32 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Set方法: entries()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - const entries = userStore.$state.persons.entries(); + const nameList: string[] = []; + const entries = userStore.$s.persons.entries(); for (const entry of entries) { nameList.push(entry[0].name); } @@ -175,32 +184,32 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Set方法: forEach()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - userStore.$state.persons.forEach(per => { + const nameList: string[] = []; + userStore.$s.persons.forEach(per => { nameList.push(per.name); }); @@ -213,53 +222,53 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Set方法: has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); }); it('测试Set方法: for of()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); - const nameList = []; - for (const per of userStore.$state.persons) { + const nameList: string[] = []; + for (const per of userStore.$s.persons) { nameList.push(per.name); } @@ -272,23 +281,23 @@ describe('测试store中的Set', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在set中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在set中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear set Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx similarity index 61% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx index 28d4ff98..2d5a441f 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx @@ -1,45 +1,55 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new WeakMap([ + [{ name: 'p1' }, 1], + [{ name: 'p2' }, 2], + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person, 3); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons = new WeakMap([]); + }, + reset: state => { + state.persons = new WeakMap([ + [{ name: 'p1' }, 1], + [{ name: 'p2' }, 2], + ]); + }, + }, +}); describe('测试store中的WeakMap', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new WeakMap([ - [{ name: 'p1' }, 1], - [{ name: 'p2' }, 2], - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.set(person, 3); - }, - delOnePerson: (state, person) => { - state.persons.delete(person); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,7 +57,7 @@ describe('测试store中的WeakMap', () => { const newPerson = { name: 'p3' }; function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -76,49 +86,49 @@ describe('测试store中的WeakMap', () => { it('测试WeakMap方法: set()、delete()、has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在WeakMap中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); // 在WeakMap中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); }); it('测试WeakMap方法: get()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: undefined'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: undefined'); // 在WeakMap中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); }); }); diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx similarity index 59% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js rename to scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx index 4c6a8fca..ecbc632d 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.js +++ b/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx @@ -1,53 +1,62 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new WeakSet([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.add(person); + }, + delOnePerson: (state, person) => { + state.persons.delete(person); + }, + clearPersons: state => { + state.persons = new WeakSet([]); + }, + reset: state => { + state.persons = new WeakSet([ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ]); + }, + }, +}); describe('测试store中的WeakSet', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new WeakSet([ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.add(person); - }, - delOnePerson: (state, person) => { - state.persons.delete(person); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); const newPerson = { name: 'p3', age: 3 }; - function Parent(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); const addOnePerson = function() { userStore.addOnePerson(newPerson); }; @@ -69,28 +78,28 @@ describe('测试store中的WeakSet', () => { it('测试WeakSet方法: add()、delete()、has()', () => { function Child(props) { - const userStore = useStore('user'); + const userStore = useUserStore(); return (
- +
); } Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在WeakSet中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); // 在WeakSet中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx similarity index 80% rename from scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx index f45599d2..66c287e9 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx @@ -1,6 +1,8 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; @@ -13,7 +15,7 @@ function postpone(timer, func) { } describe('Asynchronous functions', () => { - let container = null; + let container: HTMLElement | null = null; const COUNTER_ID = 'counter'; const TOGGLE_ID = 'toggle'; @@ -33,7 +35,7 @@ describe('Asynchronous functions', () => { return new Promise(resolve => { setTimeout(() => { state.counter++; - resolve(); + resolve(true); }, 100); }); }, @@ -53,11 +55,12 @@ describe('Asynchronous functions', () => { afterEach(() => { unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; }); it('Should wait for async actions', async () => { + // @ts-ignore jest.useRealTimers(); let globalStore; @@ -84,14 +87,14 @@ describe('Asynchronous functions', () => { Horizon.render(, container); // initial state - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); // slow toggle has nothing to wait for, it is resolved immediately Horizon.act(() => { triggerClickEvent(container, TOGGLE_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); // counter increment is slow. slow toggle waits for result Horizon.act(() => { @@ -101,18 +104,18 @@ describe('Asynchronous functions', () => { triggerClickEvent(container, TOGGLE_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); // fast toggle does not wait for counter and it is resolved immediately Horizon.act(() => { triggerClickEvent(container, TOGGLE_FAST_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); // at 150ms counter increment will be resolved and slow toggle immediately after const t150 = postpone(150, () => { - expect(document.getElementById(RESULT_ID).innerHTML).toBe('true1'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true1'); }); // before that, two more actions are added to queue - another counter and slow toggle @@ -125,13 +128,14 @@ describe('Asynchronous functions', () => { // at 250ms they should be already resolved const t250 = postpone(250, () => { - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false2'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false2'); }); await Promise.all([t150, t250]); }); it('call async action by then', async () => { + // @ts-ignore jest.useFakeTimers(); let globalStore; @@ -150,12 +154,13 @@ describe('Asynchronous functions', () => { // call async action by then globalStore.$queue.increment().then(() => { - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false1'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false1'); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); // past 150 ms + // @ts-ignore jest.advanceTimersByTime(150); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js deleted file mode 100644 index e42feeda..00000000 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import * as Horizon from '@cloudsop/horizon/index.ts'; -import { triggerClickEvent } from '../../jest/commonComponents'; -import { useLogStore } from './store'; - -const { unmountComponentAtNode } = Horizon; - -describe('Basic store manipulation', () => { - let container = null; - - const BUTTON_ID = 'btn'; - const RESULT_ID = 'result'; - - beforeEach(() => { - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - unmountComponentAtNode(container); - container.remove(); - container = null; - }); - - it('Should use getters', () => { - function App() { - const logStore = useLogStore(); - - return
{logStore.length}
; - } - - Horizon.render(, container); - - expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); - }); - - it('Should use actions and update components', () => { - function App() { - const logStore = useLogStore(); - - return ( -
- -

{logStore.length}

-
- ); - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - }); - - expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); - }); -}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx new file mode 100644 index 00000000..0a2c8584 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx @@ -0,0 +1,110 @@ +//@ts-ignore +import Horizon from '@cloudsop/horizon/index.ts'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { useLogStore } from './store'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; +import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; + +const { unmountComponentAtNode } = Horizon; + +describe('Basic store manipulation', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use getters', () => { + function App() { + const logStore = useLogStore(); + + return
{logStore.length}
; + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); + }); + + it('Should use actions and update components', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.length}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2'); + }); + + it('should call actions from own actions', () => { + const useIncrementStore = createStore({ + id: 'incrementStore', + state: { + count: 2, + }, + actions: { + increment: state => { + state.count++; + }, + doublePlusOne: function(state) { + state.count = state.count * 2; + this.increment(); + }, + }, + }); + + function App() { + const incrementStore = useIncrementStore(); + + return ( +
+ +

{incrementStore.count}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx similarity index 64% rename from scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx index 032234d0..94d149f0 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx @@ -1,11 +1,13 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; import { triggerClickEvent } from '../../jest/commonComponents'; import { useLogStore } from './store'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; describe('Dollar store access', () => { - let container = null; + let container: HTMLElement | null = null; const BUTTON_ID = 'btn'; const RESULT_ID = 'result'; @@ -17,23 +19,23 @@ describe('Dollar store access', () => { afterEach(() => { unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; }); - it('Should use $state and $computed', () => { + it('Should use $s and $c', () => { function App() { const logStore = useLogStore(); - return
{logStore.$computed.length()}
; + return
{logStore.$c.length()}
; } Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('1'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); }); - it('Should use $actions and update components', () => { + it('Should use $a and update components', () => { function App() { const logStore = useLogStore(); @@ -42,12 +44,12 @@ describe('Dollar store access', () => { -

{logStore.$computed.length()}

+

{logStore.$c.length()}

); } @@ -58,6 +60,6 @@ describe('Dollar store access', () => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('2'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2'); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx similarity index 71% rename from scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx index 115164c0..ad6a4028 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx @@ -1,11 +1,13 @@ +//@ts-ignore import * as Horizon from '@cloudsop/horizon/index.ts'; import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; describe('Self referencing', () => { - let container = null; + let container: HTMLElement | null = null; const BUTTON_ID = 'btn'; const RESULT_ID = 'result'; @@ -15,7 +17,7 @@ describe('Self referencing', () => { val: 2, }, actions: { - magic: function(state) { + increaseVal: function(state) { state.val = state.val * 2 - 1; }, }, @@ -34,7 +36,7 @@ describe('Self referencing', () => { afterEach(() => { unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; }); @@ -45,8 +47,8 @@ describe('Self referencing', () => { return (

{store.double}

-
); @@ -54,29 +56,29 @@ describe('Self referencing', () => { Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('4'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('4'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('6'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('6'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('10'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('10'); }); it('should access other stores', () => { const useOtherStore = createStore({ state: {}, actions: { - doMagic: () => useSelfRefStore().magic(), + doIncreaseVal: () => useSelfRefStore().increaseVal(), }, computed: { - magicConstant: () => useSelfRefStore().value, + selfRefStoreValue: () => useSelfRefStore().value, }, }); @@ -85,9 +87,9 @@ describe('Self referencing', () => { return (
-

{store.magicConstant}

-
); @@ -95,13 +97,13 @@ describe('Self referencing', () => { Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('5'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('9'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); }); it('should use parametric getters', () => { @@ -138,11 +140,11 @@ describe('Self referencing', () => { } Horizon.render(, container); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('abc'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('abc'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - expect(document.getElementById(RESULT_ID).innerHTML).toBe('def'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('def'); }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.js similarity index 65% rename from scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/reset.js index f0b349da..0cc4a4d3 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.test.js +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/reset.js @@ -1,14 +1,14 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { triggerClickEvent } from '../../jest/commonComponents'; +import {createStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {triggerClickEvent} from '../../jest/commonComponents'; -const { unmountComponentAtNode } = Horizon; +const {unmountComponentAtNode} = Horizon; describe('Reset', () => { it('RESET NOT IMPLEMENTED', async () => { // console.log('reset functionality is not yet implemented') expect(true).toBe(true); - }); + }) return; let container = null; @@ -19,14 +19,14 @@ describe('Reset', () => { const useCounter = createStore({ state: { - counter: 0, + counter: 0 }, actions: { - increment: function(state) { + increment: function (state) { state.counter++; - }, + } }, - computed: {}, + computed: {} }); beforeEach(() => { @@ -44,25 +44,17 @@ describe('Reset', () => { function App() { const store = useCounter(); - return ( -
-

{store.$state.counter}

- - -
- ); + return
+

{store.$s.counter}

+ + +
} - Horizon.render(, container); + Horizon.render(, container); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/store.js b/scripts/__tests__/HorizonXText/StoreFunctionality/store.ts similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/store.js rename to scripts/__tests__/HorizonXText/StoreFunctionality/store.ts diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx similarity index 59% rename from scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js rename to scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx index 2fa93598..e14c5d9e 100644 --- a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.js +++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx @@ -1,9 +1,12 @@ +//@ts-ignore +import * as Horizon from '@cloudsop/horizon/index.ts'; import { createStore, applyMiddleware, combineReducers, - bindActionCreators + bindActionCreators, } from '../../../../libs/horizon/src/horizonx/adapters/redux'; +import { describe, it, expect } from '@jest/globals'; describe('Redux adapter', () => { it('should use getState()', async () => { @@ -12,21 +15,21 @@ describe('Redux adapter', () => { }, 0); expect(reduxStore.getState()).toBe(0); - }) + }); it('Should use default state, dispatch action and update state', async () => { const reduxStore = createStore((state, action) => { switch (action.type) { - case('ADD'): - return {counter: state.counter + 1} + case 'ADD': + return { counter: state.counter + 1 }; default: - return {counter: 0}; + return { counter: 0 }; } }); expect(reduxStore.getState().counter).toBe(0); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); expect(reduxStore.getState().counter).toBe(1); }); @@ -35,37 +38,37 @@ describe('Redux adapter', () => { let counter = 0; const reduxStore = createStore((state = 0, action) => { switch (action.type) { - case('ADD'): - return state + 1 + case 'ADD': + return state + 1; default: return state; } }); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); expect(counter).toBe(0); expect(reduxStore.getState()).toBe(1); const unsubscribe = reduxStore.subscribe(() => { counter++; }); - reduxStore.dispatch({type: 'ADD'}); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); + reduxStore.dispatch({ type: 'ADD' }); expect(counter).toBe(2); expect(reduxStore.getState()).toBe(3); unsubscribe(); - reduxStore.dispatch({type: 'ADD'}); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); + reduxStore.dispatch({ type: 'ADD' }); expect(counter).toBe(2); expect(reduxStore.getState()).toBe(5); }); it('Should bind action creators', async () => { - const addTodo = (text) => { + const addTodo = text => { return { type: 'ADD_TODO', - text - } - } + text, + }; + }; const reduxStore = createStore((state = [], action) => { if (action.type === 'ADD_TODO') { @@ -74,7 +77,7 @@ describe('Redux adapter', () => { return state; }); - const actions = bindActionCreators({addTodo}, reduxStore.dispatch); + const actions = bindActionCreators({ addTodo }, reduxStore.dispatch); actions.addTodo('todo'); @@ -84,57 +87,57 @@ describe('Redux adapter', () => { it('Should replace reducer', async () => { const reduxStore = createStore((state, action) => { switch (action.type) { - case('ADD'): - return {counter: state.counter + 1} + case 'ADD': + return { counter: state.counter + 1 }; default: - return {counter: 0}; + return { counter: 0 }; } }); - reduxStore.dispatch({type: 'ADD'}); + reduxStore.dispatch({ type: 'ADD' }); expect(reduxStore.getState().counter).toBe(1); reduxStore.replaceReducer((state, action) => { switch (action.type) { - case('SUB'): - return {counter: state.counter - 1} + case 'SUB': + return { counter: state.counter - 1 }; default: - return {counter: 0}; + return { counter: 0 }; } }); - reduxStore.dispatch({type: 'SUB'}); + reduxStore.dispatch({ type: 'SUB' }); expect(reduxStore.getState().counter).toBe(0); - }) + }); it('Should combine reducers', async () => { const booleanReducer = (state = false, action) => { switch (action.type) { - case('TOGGLE'): - return !state - default: - return state; - } - } - - const addReducer = (state = 0, action) => { - switch (action.type) { - case('ADD'): - return state + 1 + case 'TOGGLE': + return !state; default: return state; } }; - const reduxStore = createStore(combineReducers({check: booleanReducer, counter: addReducer})); + const addReducer = (state = 0, action) => { + switch (action.type) { + case 'ADD': + return state + 1; + default: + return state; + } + }; + + const reduxStore = createStore(combineReducers({ check: booleanReducer, counter: addReducer })); expect(reduxStore.getState().counter).toBe(0); expect(reduxStore.getState().check).toBe(false); - reduxStore.dispatch({type: 'ADD'}); - reduxStore.dispatch({type: 'TOGGLE'}); + reduxStore.dispatch({ type: 'ADD' }); + reduxStore.dispatch({ type: 'TOGGLE' }); expect(reduxStore.getState().counter).toBe(1); expect(reduxStore.getState().check).toBe(true); @@ -142,28 +145,32 @@ describe('Redux adapter', () => { it('Should apply enhancers', async () => { let counter = 0; - let middlewareCallList = []; + let middlewareCallList: string[] = []; const callCounter = store => next => action => { middlewareCallList.push('callCounter'); counter++; let result = next(action); return result; - } + }; - const reduxStore = createStore((state, action) => { - switch (action.type) { - case('toggle'): - return { - check: !state.check - } - default: - return state; - } - }, {check: false}, applyMiddleware(callCounter)); + const reduxStore = createStore( + (state, action) => { + switch (action.type) { + case 'toggle': + return { + check: !state.check, + }; + default: + return state; + } + }, + { check: false }, + applyMiddleware(callCounter) + ); - reduxStore.dispatch({type: 'toggle'}); - reduxStore.dispatch({type: 'toggle'}); + reduxStore.dispatch({ type: 'toggle' }); + reduxStore.dispatch({ type: 'toggle' }); expect(counter).toBe(3); // NOTE: first action is always store initialization }); @@ -171,38 +178,42 @@ describe('Redux adapter', () => { it('Should apply multiple enhancers', async () => { let counter = 0; let lastAction = ''; - let middlewareCallList = []; + let middlewareCallList: string[] = []; const callCounter = store => next => action => { middlewareCallList.push('callCounter'); counter++; let result = next(action); return result; - } + }; const lastFunctionStorage = store => next => action => { middlewareCallList.push('lastFunctionStorage'); lastAction = action.type; let result = next(action); return result; - } + }; - const reduxStore = createStore((state, action) => { - switch (action.type) { - case('toggle'): - return { - check: !state.check - } - default: - return state; - } - }, {check: false}, applyMiddleware(callCounter, lastFunctionStorage)); + const reduxStore = createStore( + (state, action) => { + switch (action.type) { + case 'toggle': + return { + check: !state.check, + }; + default: + return state; + } + }, + { check: false }, + applyMiddleware(callCounter, lastFunctionStorage) + ); - reduxStore.dispatch({type: 'toggle'}); + reduxStore.dispatch({ type: 'toggle' }); expect(counter).toBe(2); // NOTE: first action is always store initialization expect(lastAction).toBe('toggle'); - expect(middlewareCallList[0]).toBe("callCounter"); - expect(middlewareCallList[1]).toBe("lastFunctionStorage"); + expect(middlewareCallList[0]).toBe('callCounter'); + expect(middlewareCallList[1]).toBe('lastFunctionStorage'); }); }); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js deleted file mode 100644 index 3785b92c..00000000 --- a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import { createStore, applyMiddleware, thunk } from '../../../../libs/horizon/src/horizonx/adapters/redux'; - -describe('Redux thunk', () => { - it('should use apply thunk middleware', async () => { - const MAX_TODOS = 5; - - function addTodosIfAllowed(todoText) { - return (dispatch, getState) => { - const state = getState(); - - if (state.todos.length < MAX_TODOS) { - dispatch({ type: 'ADD_TODO', text: todoText }); - } - }; - } - - const todoStore = createStore( - (state = { todos: [] }, action) => { - if (action.type === 'ADD_TODO') { - return { todos: state.todos?.concat(action.text) }; - } - return state; - }, - null, - applyMiddleware(thunk) - ); - - for (let i = 0; i < 10; i++) { - todoStore.dispatch(addTodosIfAllowed('todo no.' + i)); - } - - expect(todoStore.getState().todos.length).toBe(5); - }); -}); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx new file mode 100644 index 00000000..39e4fc83 --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx @@ -0,0 +1,33 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import { createStore, applyMiddleware, thunk } from '../../../../libs/horizon/src/horizonx/adapters/redux'; +import {describe, it, expect} from '@jest/globals'; + +describe('Redux thunk', () => { + it('should use apply thunk middleware', async () => { + const MAX_TODOS = 5; + + function addTodosIfAllowed(todoText) { + return (dispatch, getState) => { + const state = getState(); + + if (state.todos.length < MAX_TODOS) { + dispatch({type: 'ADD_TODO', text: todoText}); + } + } + } + + const todoStore = createStore((state = {todos: []}, action) => { + if (action.type === 'ADD_TODO') { + return {todos: state.todos?.concat(action.text)}; + } + return state; + }, null, applyMiddleware(thunk)); + + for (let i = 0; i < 10; i++) { + //TODO: resolve thunk problems + (todoStore.dispatch as unknown as (delayedAction:(dispatch,getState)=>void)=>void)(addTodosIfAllowed('todo no.' + i)); + } + + expect(todoStore.getState().todos.length).toBe(5); + }); +}); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx similarity index 50% rename from scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js rename to scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx index 5977671a..4f6feec6 100644 --- a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.js +++ b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx @@ -1,3 +1,4 @@ +//@ts-ignore import horizon, * as Horizon from '@cloudsop/horizon/index.ts'; import { batch, @@ -8,17 +9,19 @@ import { useSelector, useStore, createSelectorHook, - createDispatchHook, + createDispatchHook } from '../../../../libs/horizon/src/horizonx/adapters/redux'; -import { triggerClickEvent } from '../../jest/commonComponents'; +import {triggerClickEvent} from '../../jest/commonComponents'; +import {describe, it, beforeEach, afterEach, expect} from '@jest/globals'; +import { ReduxStoreHandler } from '@cloudsop/horizon/src/horizonx/types'; const BUTTON = 'button'; const BUTTON2 = 'button2'; const RESULT = 'result'; -const CONTAINER = 'container'; +const CONTAINER = 'container' -function getE(id) { - return document.getElementById(id); +function getE(id):HTMLElement { + return document.getElementById(id)||document.body; } describe('Redux/React binding adapter', () => { @@ -36,19 +39,17 @@ describe('Redux/React binding adapter', () => { const reduxStore = createStore((state = 'state', action) => state); const Child = () => { - const store = useStore(); + const store = useStore() as unknown as ReduxStoreHandler; return
{store.getState()}
; }; const Wrapper = () => { - return ( - - - - ); + return + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('state'); }); @@ -60,30 +61,23 @@ describe('Redux/React binding adapter', () => { }); const Child = () => { - const store = useStore(); + const store = useStore() as unknown as ReduxStoreHandler; const dispatch = useDispatch(); - return ( -
-

{store.getState()}

- -
- ); + return
+

{store.getState()}

+ +
; }; const Wrapper = () => { - return ( - - - - ); + return + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(reduxStore.getState()).toBe(0); @@ -101,32 +95,24 @@ describe('Redux/React binding adapter', () => { }); const Child = () => { - const count = useSelector(state => state); + const count = useSelector((state) => state); const dispatch = useDispatch(); - return ( -
-

{count}

- -
- ); + return
+

{count}

+ +
; }; const Wrapper = () => { - return ( - - - - ); + return + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('0'); @@ -139,80 +125,58 @@ describe('Redux/React binding adapter', () => { }); it('Should use connect', async () => { - const reduxStore = createStore( - (state, action) => { - switch (action.type) { - case 'INCREMENT': - return { - ...state, - value: state.negative ? state.value - action.amount : state.value + action.amount, - }; - case 'TOGGLE': - return { - ...state, - negative: !state.negative, - }; - default: - return state; - } - }, - { negative: false, value: 0 } - ); + const reduxStore = createStore((state, action) => { + switch (action.type) { + case('INCREMENT'): + return { + ...state, + value: state.negative ? state.value - action.amount : state.value + action.amount + }; + case('TOGGLE'): + return { + ...state, + negative: !state.negative + }; + default: + return state; + } + }, {negative: false, value: 0}); - const Child = connect( - (state, ownProps) => { - // map state to props - return { ...state, ...ownProps }; - }, - (dispatch, ownProps) => { - // map dispatch to props - return { - increment: () => dispatch({ type: 'INCREMENT', amount: ownProps.amount }), - }; - }, - (stateProps, dispatchProps, ownProps) => { - //merge props - return { stateProps, dispatchProps, ownProps }; - }, - {} - )(props => { + const Child = connect((state, ownProps) => { + // map state to props + return {...state, ...ownProps}; + }, (dispatch, ownProps) => { + // map dispatch to props + return { + // @ts-ignore + increment: () => dispatch({type: 'INCREMENT', amount: ownProps?.amount}) + }; + }, (stateProps, dispatchProps, ownProps) => { + //merge props + return {stateProps, dispatchProps, ownProps}; + }, {})((props) => { const n = props.stateProps.negative; - return ( -
-
- {n ? '-' : '+'} - {props.stateProps.value} -
- -
- ); + return
+
{n ? '-' : '+'}{props.stateProps.value}
+ +
; }); const Wrapper = () => { + //@ts-ignore const [amount, setAmount] = Horizon.useState(5); - return ( - - - - - ); + return + + + ; }; - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('+0'); @@ -231,7 +195,7 @@ describe('Redux/React binding adapter', () => { }); expect(getE(RESULT).innerHTML).toBe('+8'); - }); + }) it('Should batch dispatches', async () => { const reduxStore = createStore((state = 0, action) => { @@ -244,32 +208,22 @@ describe('Redux/React binding adapter', () => { function Counter() { renderCounter++; - const value = useSelector(state => state); + const value = useSelector((state) => state); const dispatch = useDispatch(); - return ( -
-

{value}

- -
- ); + return
+

{value}

+ +
; } - Horizon.render( - - - , - getE(CONTAINER) - ); + Horizon.render(, getE(CONTAINER)); expect(getE(RESULT).innerHTML).toBe('0'); expect(renderCounter).toBe(1); @@ -300,49 +254,33 @@ describe('Redux/React binding adapter', () => { const count = createSelectorHook(counterContext)(); const dispatch = createDispatchHook(counterContext)(); - return ( - - ); + return ; } function Toggle() { const check = createSelectorHook(toggleContext)(); const dispatch = createDispatchHook(toggleContext)(); - return ( - - ); + return ; } function Wrapper() { - return ( -
- - - + return
+ + + - - - -
- ); + + + +
; } - Horizon.render(, getE(CONTAINER)); + Horizon.render(, getE(CONTAINER)); expect(getE(BUTTON).innerHTML).toBe('0'); expect(getE(BUTTON2).innerHTML).toBe('false'); diff --git a/scripts/__tests__/HorizonXText/class/ClassException.test.js b/scripts/__tests__/HorizonXText/class/ClassException.test.tsx similarity index 54% rename from scripts/__tests__/HorizonXText/class/ClassException.test.js rename to scripts/__tests__/HorizonXText/class/ClassException.test.tsx index 64cea5aa..7933d376 100644 --- a/scripts/__tests__/HorizonXText/class/ClassException.test.js +++ b/scripts/__tests__/HorizonXText/class/ClassException.test.tsx @@ -1,14 +1,17 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { Text } from '../../jest/commonComponents'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {Text, triggerClickEvent} from '../../jest/commonComponents'; +import {getObserver} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; describe('测试 Class VNode 清除时,对引用清除', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; + const {unmountComponentAtNode} = Horizon; + let container:HTMLElement|null = null; let globalState = { name: 'bing dun dun', isWin: true, - isShow: true, + isShow: true }; beforeEach(() => { @@ -23,7 +26,7 @@ describe('测试 Class VNode 清除时,对引用清除', () => { setWin: (state, val) => { state.isWin = val; }, - hide: state => { + hide: (state) => { state.isShow = false; }, updateName: (state, val) => { @@ -36,8 +39,9 @@ describe('测试 Class VNode 清除时,对引用清除', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,23 +51,21 @@ describe('测试 Class VNode 清除时,对引用清除', () => { userStore = useStore('user'); render() { + if(!this.userStore) return
; // Do not modify the store data in the render method. Otherwise, an infinite loop may occur. this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun'); - return ( -
- - -
- ); + return
+ + +
; } } expect(() => { - Horizon.render(, container); - }).toThrow( - 'The number of updates exceeds the upper limit 50.\n' + - ' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.' - ); + Horizon.render(, container); + }).toThrow('The number of updates exceeds the upper limit 50.\n' + + ' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.'); + }); }); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js deleted file mode 100644 index c61aed9c..00000000 --- a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.js +++ /dev/null @@ -1,220 +0,0 @@ -import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; - -describe('在Class组件中,测试store中的Array', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; - beforeEach(() => { - // 创建一个 DOM 元素作为渲染目标 - container = document.createElement('div'); - document.body.appendChild(container); - - const persons = [ - { name: 'p1', age: 1 }, - { name: 'p2', age: 2 }, - ]; - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.push(person); - }, - delOnePerson: state => { - state.persons.pop(); - }, - clearPersons: state => { - state.persons = null; - }, - }, - }); - }); - - afterEach(() => { - // 退出时进行清理 - unmountComponentAtNode(container); - container.remove(); - container = null; - - clearStore('user'); - }); - - const newPerson = { name: 'p3', age: 3 }; - - class Parent extends Horizon.Component { - userStore = useStore('user'); - - addOnePerson = () => { - this.userStore.addOnePerson(newPerson); - }; - - delOnePerson = () => { - this.userStore.delOnePerson(); - }; - - render() { - return ( -
- - -
{this.props.children}
-
- ); - } - } - - it('测试Array方法: push()、pop()', () => { - class Child extends Horizon.Component { - userStore = useStore('user'); - - render() { - return ( -
- -
- ); - } - } - - Horizon.render(, container); - - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - // 在Array中增加一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'addBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3'); - - // 在Array中删除一个对象 - Horizon.act(() => { - triggerClickEvent(container, 'delBtn'); - }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2'); - }); - - it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { - let globalStore = null; - - class Child extends Horizon.Component { - userStore = useStore('user'); - - constructor(props) { - super(props); - globalStore = this.userStore; - } - - render() { - const nameList = []; - const entries = this.userStore.$state.persons?.entries(); - if (entries) { - for (const entry of entries) { - nameList.push(entry[1].name); - } - } - - return ( -
- -
- ); - } - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); - - it('测试Array方法: forEach()', () => { - let globalStore = null; - - class Child extends Horizon.Component { - userStore = useStore('user'); - - constructor(props) { - super(props); - globalStore = this.userStore; - } - - render() { - const nameList = []; - this.userStore.$state.persons?.forEach(per => { - nameList.push(per.name); - }); - - return ( -
- -
- ); - } - } - - Horizon.render(, container); - - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); - // push - globalStore.$state.persons.push(newPerson); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); - - // shift - globalStore.$state.persons.shift({ name: 'p0', age: 0 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3'); - - // 赋值[2] - globalStore.$state.persons[2] = { name: 'p4', age: 4 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4'); - - // 重新赋值[2] - globalStore.$state.persons[2] = { name: 'p5', age: 5 }; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5'); - - // unshift - globalStore.$state.persons.unshift({ name: 'p1', age: 1 }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5'); - - // 重新赋值 null - globalStore.$state.persons = null; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); - - // 重新赋值 [{ name: 'p1', age: 1 }] - globalStore.$state.persons = [{ name: 'p1', age: 1 }]; - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1'); - }); -}); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx new file mode 100644 index 00000000..2ca08ef5 --- /dev/null +++ b/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx @@ -0,0 +1,231 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {App, Text, triggerClickEvent} from '../../jest/commonComponents'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; + +type Person = {name:string,age:number}; + +const persons:Person[] = [{ name: 'p1', age: 1 }, { name: 'p2', age: 2 }]; +let useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: persons, + }, + actions: { + addOnePerson: (state, person) => { + state.persons.push(person); + }, + delOnePerson: (state) => { + state.persons.pop(); + }, + clearPersons: (state) => { + state.persons = []; + }, + }, +}); + +describe('在Class组件中,测试store中的Array', () => { + const { unmountComponentAtNode } = Horizon; + let container:HTMLElement|null = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container?.remove(); + container = null; + LogUtils.clear(); + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + class Parent extends Horizon.Component { + userStore = useUserStore(); + props:{ + children:any[] + }; + + constructor(props){ + super(props); + this.props = props; + } + + addOnePerson = () => { + this.userStore.addOnePerson(newPerson); + } + + delOnePerson = () => { + this.userStore.delOnePerson(); + } + + render() { + return
+ + +
+ {this.props.children} +
+
+ } + } + + it('测试Array方法: push()、pop()', () => { + class Child extends Horizon.Component { + userStore = useUserStore(); + + render() { + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + // 在Array中增加一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'addBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3'); + + // 在Array中删除一个对象 + Horizon.act(() => { + triggerClickEvent(container, 'delBtn'); + }); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2'); + }); + + it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => { + let globalStore = useUserStore(); + + class Child extends Horizon.Component { + userStore = useUserStore(); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList:string[] = []; + const entries = this.userStore.$s.persons?.entries(); + if (entries) { + for (const entry of entries) { + nameList.push(entry[1].name); + } + } + + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore?.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + // @ts-ignore TODO:why has this function argument? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + + it('测试Array方法: forEach()', () => { + let globalStore = useUserStore(); + globalStore.$s.persons.push({ name: 'p2', age: 2 }); + class Child extends Horizon.Component { + userStore = useUserStore(); + + constructor(props) { + super(props); + globalStore = this.userStore; + } + + render() { + const nameList:string[] = []; + this.userStore.$s.persons.forEach((per:Person) => { + nameList.push(per.name); + }); + + return ( +
+ +
+ ); + } + } + + Horizon.render(, container); + + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); + // push + globalStore.$s.persons.push(newPerson); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); + + // shift + // @ts-ignore TODO:why has this function argument? + globalStore.$s.persons.shift({ name: 'p0', age: 0 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3'); + + // 赋值[2] + globalStore.$s.persons[2] = { name: 'p4', age: 4 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p4'); + + // 重新赋值[2] + globalStore.$s.persons[2] = { name: 'p5', age: 5 }; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3 p5'); + + // unshift + globalStore.$s.persons.unshift({ name: 'p1', age: 1 }); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3 p5'); + + // 重新赋值 [] + globalStore.$s.persons = []; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); + + // 重新赋值 [{ name: 'p1', age: 1 }] + globalStore.$s.persons = [{ name: 'p1', age: 1 }]; + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1'); + }); + +}); diff --git a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx similarity index 58% rename from scripts/__tests__/HorizonXText/class/ClassStateMap.test.js rename to scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx index 071ad650..bb790d8a 100644 --- a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.js +++ b/scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx @@ -1,45 +1,48 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {App, Text, triggerClickEvent} from '../../jest/commonComponents'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; + +const useUserStore = createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: new Map([['p1', 1], ['p2', 2]]), + }, + actions: { + addOnePerson: (state, person) => { + state.persons.set(person.name, person.age); + }, + delOnePerson: (state, person) => { + state.persons.delete(person.name); + }, + clearPersons: (state) => { + state.persons.clear(); + }, + reset: (state)=>{ + state.persons=new Map([['p1', 1], ['p2', 2]]); + } + }, +}); describe('在Class组件中,测试store中的Map', () => { const { unmountComponentAtNode } = Horizon; - let container = null; + let container:HTMLElement|null = null; beforeEach(() => { // 创建一个 DOM 元素作为渲染目标 container = document.createElement('div'); document.body.appendChild(container); - const persons = new Map([ - ['p1', 1], - ['p2', 2], - ]); - - createStore({ - id: 'user', - state: { - type: 'bing dun dun', - persons: persons, - }, - actions: { - addOnePerson: (state, person) => { - state.persons.set(person.name, person.age); - }, - delOnePerson: (state, person) => { - state.persons.delete(person.name); - }, - clearPersons: state => { - state.persons.clear(); - }, - }, - }); + useUserStore().reset(); }); afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -47,17 +50,23 @@ describe('在Class组件中,测试store中的Map', () => { const newPerson = { name: 'p3', age: 3 }; class Parent extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); + props = {children:[]} + + constructor(props){ + super(props); + this.props = props; + } addOnePerson = () => { this.userStore.addOnePerson(newPerson); - }; + } delOnePerson = () => { this.userStore.delOnePerson(newPerson); - }; + } clearPersons = () => { this.userStore.clearPersons(); - }; + } render() { return ( @@ -71,7 +80,9 @@ describe('在Class组件中,测试store中的Map', () => { -
{this.props.children}
+
+ {this.props.children} +
); } @@ -79,12 +90,12 @@ describe('在Class组件中,测试store中的Map', () => { it('测试Map方法: set()、delete()、clear()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { return (
- +
); } @@ -92,33 +103,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 3'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 2'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#size').innerHTML).toBe('persons number: 0'); + expect(container?.querySelector('#size')?.innerHTML).toBe('persons number: 0'); }); it('测试Map方法: keys()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - const keys = this.userStore.$state.persons.keys(); + const nameList:string[] = []; + const keys = this.userStore.$s.persons.keys(); for (const key of keys) { nameList.push(key); } @@ -133,33 +144,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: values()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const ageList = []; - const values = this.userStore.$state.persons.values(); + const ageList:number[] = []; + const values = this.userStore.$s.persons.values(); for (const val of values) { ageList.push(val); } @@ -174,33 +185,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2 3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2'); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: 1 2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#ageList').innerHTML).toBe('age list: '); + expect(container?.querySelector('#ageList')?.innerHTML).toBe('age list: '); }); it('测试Map方法: entries()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - const entries = this.userStore.$state.persons.entries(); + const nameList:string[] = []; + const entries = this.userStore.$s.persons.entries(); for (const entry of entries) { nameList.push(entry[0]); } @@ -215,33 +226,33 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: forEach()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - this.userStore.$state.persons.forEach((val, key) => { + const nameList:string[] = []; + this.userStore.$s.persons.forEach((val, key) => { nameList.push(key); }); @@ -255,34 +266,34 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); it('测试Map方法: has()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { return (
- +
); } @@ -290,21 +301,21 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: false'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true'); + expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: true'); }); it('测试Map方法: for of()', () => { class Child extends Horizon.Component { - userStore = useStore('user'); + userStore = useUserStore(); render() { - const nameList = []; - for (const per of this.userStore.$state.persons) { + const nameList:string[] = []; + for (const per of this.userStore.$s.persons) { nameList.push(per[0]); } @@ -318,23 +329,23 @@ describe('在Class组件中,测试store中的Map', () => { Horizon.render(, container); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // 在Map中增加一个对象 Horizon.act(() => { triggerClickEvent(container, 'addBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3'); // 在Map中删除一个对象 Horizon.act(() => { triggerClickEvent(container, 'delBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2'); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2'); // clear Map Horizon.act(() => { triggerClickEvent(container, 'clearBtn'); }); - expect(container.querySelector('#nameList').innerHTML).toBe('name list: '); + expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: '); }); }); diff --git a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx similarity index 62% rename from scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js rename to scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx index a2226b1c..4fe4e719 100644 --- a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.js +++ b/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx @@ -1,15 +1,17 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { Text, triggerClickEvent } from '../../jest/commonComponents'; -import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {Text, triggerClickEvent} from '../../jest/commonComponents'; +import {getObserver} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {describe, it, beforeEach, afterEach, expect} from '@jest/globals'; describe('测试 Class VNode 清除时,对引用清除', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; + const {unmountComponentAtNode} = Horizon; + let container:HTMLElement|null = null; let globalState = { name: 'bing dun dun', isWin: true, - isShow: true, + isShow: true }; beforeEach(() => { @@ -24,7 +26,7 @@ describe('测试 Class VNode 清除时,对引用清除', () => { setWin: (state, val) => { state.isWin = val; }, - hide: state => { + hide: (state) => { state.isShow = false; }, updateName: (state, val) => { @@ -37,8 +39,9 @@ describe('测试 Class VNode 清除时,对引用清除', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -48,14 +51,12 @@ describe('测试 Class VNode 清除时,对引用清除', () => { userStore = useStore('user'); render() { - return ( -
- - {this.userStore.isShow && } -
- ); + return
+ + {this.userStore?.isShow && } +
; } } @@ -63,18 +64,16 @@ describe('测试 Class VNode 清除时,对引用清除', () => { userStore = useStore('user'); setWin = () => { - this.userStore.setWin(!this.userStore.isWin); - }; + this.userStore?.setWin(!this.userStore?.isWin); + } render() { - return ( -
- - {this.userStore.isWin && } -
- ); + return
+ + {this.userStore?.isWin && } +
; } } @@ -84,16 +83,14 @@ describe('测试 Class VNode 清除时,对引用清除', () => { render() { // this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun'); - return ( -
- - -
- ); + return
+ + +
; } } - Horizon.render(, container); + Horizon.render(, container); // Parent and Child hold the isWin key expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); diff --git a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx similarity index 59% rename from scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js rename to scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx index ead0453a..76633b76 100644 --- a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.js +++ b/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx @@ -1,15 +1,17 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; -import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; -import { Text, triggerClickEvent } from '../../jest/commonComponents'; -import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import * as LogUtils from '../../jest/logUtils'; +import {clearStore, createStore, useStore} from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import {Text, triggerClickEvent} from '../../jest/commonComponents'; +import {getObserver} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {describe, it, beforeEach, afterEach, expect} from '@jest/globals'; describe('测试VNode清除时,对引用清除', () => { - const { unmountComponentAtNode } = Horizon; - let container = null; + const {unmountComponentAtNode} = Horizon; + let container:HTMLElement|null = null; let globalState = { name: 'bing dun dun', isWin: true, - isShow: true, + isShow: true }; beforeEach(() => { @@ -24,9 +26,9 @@ describe('测试VNode清除时,对引用清除', () => { setWin: (state, val) => { state.isWin = val; }, - hide: state => { + hide: (state) => { state.isShow = false; - }, + } }, }); }); @@ -34,8 +36,9 @@ describe('测试VNode清除时,对引用清除', () => { afterEach(() => { // 退出时进行清理 unmountComponentAtNode(container); - container.remove(); + container?.remove(); container = null; + LogUtils.clear(); clearStore('user'); }); @@ -45,14 +48,12 @@ describe('测试VNode清除时,对引用清除', () => { userStore = useStore('user'); render() { - return ( -
- - {this.userStore.isShow && } -
- ); + return
+ + {this.userStore?.isShow && } +
; } } @@ -60,18 +61,16 @@ describe('测试VNode清除时,对引用清除', () => { userStore = useStore('user'); setWin = () => { - this.userStore.setWin(!this.userStore.isWin); - }; + this.userStore?.setWin(!this.userStore.isWin); + } render() { - return ( -
- - {this.userStore.isWin && } -
- ); + return
+ + {this.userStore?.isWin && } +
; } } @@ -79,16 +78,14 @@ describe('测试VNode清除时,对引用清除', () => { userStore = useStore('user'); render() { - return ( -
- - -
- ); + return
+ + +
; } } - Horizon.render(, container); + Horizon.render(, container); // Parent and Child hold the isWin key expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2); diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js deleted file mode 100644 index a643f311..00000000 --- a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { createProxy } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; - -describe('Proxy', () => { - const arr = []; - - it('Should not double wrap proxies', async () => { - const proxy1 = createProxy(arr); - - const proxy2 = createProxy(proxy1); - - expect(proxy1 === proxy2).toBe(true); - }); - - it('Should re-use existing proxy of same object', async () => { - const proxy1 = createProxy(arr); - - const proxy2 = createProxy(arr); - - expect(proxy1 === proxy2).toBe(true); - }); -}); diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx new file mode 100644 index 00000000..d402e35c --- /dev/null +++ b/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx @@ -0,0 +1,48 @@ +import {createProxy} from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler'; +import {readonlyProxy} from '../../../../libs/horizon/src/horizonx/proxy/readonlyProxy'; +import {describe, beforeEach, afterEach, it, expect} from '@jest/globals'; + +describe('Proxy', () => { + const arr = []; + + it('Should not double wrap proxies', async () => { + const proxy1 = createProxy(arr); + + const proxy2 = createProxy(proxy1); + + expect(proxy1 === proxy2).toBe(true); + }); + + it('Should re-use existing proxy of same object', async () => { + const proxy1 = createProxy(arr); + + const proxy2 = createProxy(arr); + + expect(proxy1 === proxy2).toBe(true); + }); + + it('Readonly proxy should prevent changes', async () => { + const proxy1 = readonlyProxy([1]); + + try{ + proxy1.push('a'); + expect(true).toBe(false);//we expect exception above + }catch(e){ + //expected + } + + try{ + proxy1[0]=null; + expect(true).toBe(false);//we expect exception above + }catch(e){ + //expected + } + + try{ + delete proxy1[0]; + expect(true).toBe(false);//we expect exception above + }catch(e){ + //expected + } + }); +}); diff --git a/scripts/__tests__/jest/logUtils.js b/scripts/__tests__/jest/logUtils.js new file mode 100644 index 00000000..ecb32c98 --- /dev/null +++ b/scripts/__tests__/jest/logUtils.js @@ -0,0 +1,26 @@ +let dataArray = null; + +const log = value => { + if (dataArray === null) { + dataArray = [value]; + } else { + dataArray.push(value); + } +}; + +const getAndClear = () => { + if (dataArray === null) { + return []; + } + const values = dataArray; + dataArray = null; + return values; +}; + +const clear = () => { + dataArray = dataArray ? null : dataArray; +}; + +exports.clear = clear; +exports.log = log; +exports.getAndClear = getAndClear; diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index c3c31b49..c58fa290 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -4,6 +4,7 @@ import path from 'path'; import fs from 'fs'; import replace from '@rollup/plugin-replace'; import copy from './copy-plugin'; +import execute from 'rollup-plugin-execute'; import { terser } from 'rollup-plugin-terser'; import { version as horizonVersion } from '@cloudsop/horizon/package.json'; @@ -58,6 +59,7 @@ function genConfig(mode) { }, preventAssignment: true, }), + execute('npm run build-types'), mode === 'production' && terser(), copy([ { From 8995659ea50434cf60cacf6c12ba0c0439fc8945 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 29 Jul 2022 20:38:43 +0800 Subject: [PATCH 02/19] Match-id-204bc47828a04bfb747ed16f9dbd8d1885dddb7d --- .../src/horizonx/adapters/reduxReact.ts | 11 +++-- .../src/horizonx/store/StoreHandler.ts | 12 ++--- .../StoreFunctionality/basicAccess.test.tsx | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index 8e205eb7..0954fbb3 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -33,12 +33,11 @@ export function createSelectorHook(context: Context): (selector?: (any) => any) return function(selector = state => state) { const [b, fr] = useState(false); - const listener = () => { - fr(!b); - }; - useEffect(() => { - return store.subscribe(listener); + const unsubscribe = store.subscribe(() => fr(!b)); + return () => { + unsubscribe(); + }; }); return selector(store.getState()); @@ -110,7 +109,7 @@ export function connect( useEffect(() => { const unsubscribe = store.subscribe(() => forceReload(!f)); - () => { + return () => { unsubscribe(); }; }); diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index ffc67aa8..ecbe22d9 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -15,7 +15,7 @@ function isPromise(obj: any): boolean { type StoreConfig, C extends UserComputedValues> = { state?: S; - options?: { suppressHooks?: boolean }; + // options?: { suppressHooks?: boolean }; actions?: A; id?: string; computed?: C; @@ -27,18 +27,18 @@ export type ReduxStoreHandler = { getState: () => any; subscribe: (listener: () => void) => () => void; replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; - _horizonXstore: StoreHandler; + // _horizonXstore: StoreHandler; }; type StoreHandler, C extends UserComputedValues> = { $subscribe: (listener: () => void) => void; $unsubscribe: (listener: () => void) => void; $s: S; - $config: StoreConfig; + // $config: StoreConfig; $queue: QueuedStoreActions; $a: StoreActions; $c: UserComputedValues; - reduxHandler?: ReduxStoreHandler; + // reduxHandler?: ReduxStoreHandler; } & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; @@ -256,13 +256,15 @@ function hookStore() { function createStoreHook, C extends UserComputedValues>( storeHandler: StoreHandler ): () => StoreHandler { - return () => { + const storeHook = () => { if (!storeHandler.$config.options?.suppressHooks) { hookStore(); } return storeHandler; }; + + return storeHook; } export function useStore, C extends UserComputedValues>( diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx index 0a2c8584..9d95c95f 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx @@ -107,4 +107,49 @@ describe('Basic store manipulation', () => { expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); }); + + it('should call computed from own actions', () => { + const useIncrementStore = createStore({ + id: 'incrementStore', + state: { + count: 2, + }, + actions: { + doublePlusOne: function(state) { + state.count = this.double + 1; + }, + }, + computed:{ + double: (state) => { + return state.count*2 + } + } + }); + + function App() { + const incrementStore = useIncrementStore(); + + return ( +
+ +

{incrementStore.count}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); + }) }); From ccf66f1653c4aee2497db67ca579ab804f1d2593 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 15 Aug 2022 20:47:47 +0800 Subject: [PATCH 03/19] Match-id-5f3ff7a11a13ae24c88d6483333762208df08e28 --- libs/horizon/src/horizonx/adapters/redux.ts | 44 +++- .../adapters/reduxPromiseMiddleware.ts | 0 .../src/horizonx/proxy/ProxyHandler.ts | 4 +- .../proxy/handlers/ObjectProxyHandler.ts | 8 +- .../src/horizonx/store/StoreHandler.ts | 2 +- .../StoreFunctionality/async.test.tsx | 209 ++++++------------ .../adapters/ReduxAdapterPromiseMiddleware.js | 96 -------- 7 files changed, 121 insertions(+), 242 deletions(-) delete mode 100644 libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts delete mode 100644 scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index fb19cf9a..4372e0fa 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -32,6 +32,43 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; +function mergeData(state,data){ + console.log('merging data',{state,data}); + if(!data){ + console.log('!data'); + state.stateWrapper=data; + return; + } + + if(Array.isArray(data) && Array.isArray(state?.stateWrapper)){ + console.log('data is array'); + state.stateWrapper.length = data.length; + data.forEach((item,idx) => { + if(item!=state.stateWrapper[idx]){ + state.stateWrapper[idx]=item; + } + }); + return; + } + + if(typeof data === 'object' && typeof state?.stateWrapper === 'object'){ + console.log('data is object'); + Object.keys(state.stateWrapper).forEach(key => { + if(!data.hasOwnProperty(key)) delete state.stateWrapper[key]; + }) + + Object.entries(data).forEach(([key,value])=>{ + if(state.stateWrapper[key]!==value){ + state.stateWrapper[key]=value; + } + }); + return; + } + + console.log('data is primitive or type mismatch'); + state.stateWrapper = data; +} + export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', @@ -48,14 +85,19 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): if (result === undefined) { return; } // NOTE: reducer should never return undefined, in this case, do not change state + // mergeData(state,result); state.stateWrapper = result; }, }, options: { - suppressHooks: true, + reduxAdapter: true, }, })(); + // store.$subscribe(()=>{ + // console.log('changed'); + // }); + const result = { reducer, getState: function() { diff --git a/libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts b/libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index a540e199..f67dcc47 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -38,7 +38,9 @@ export function createProxy(rawObj: any, hookObserver = true): any { // 创建Proxy let proxyObj; - if (isArray(rawObj)) { + if (!hookObserver) { + proxyObj = createObjectProxy(rawObj,true); + } else if (isArray(rawObj)) { // 数组 proxyObj = createArrayProxy(rawObj as []); } else if (isCollection(rawObj)) { diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 28749a57..4dc27a0b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -2,16 +2,16 @@ import { isSame } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; -export function createObjectProxy(rawObj: T): ProxyHandler { +export function createObjectProxy(rawObj: T, singleLevel = false): ProxyHandler { const proxy = new Proxy(rawObj, { - get, + get: (...args) => get(...args, singleLevel), set, }); return proxy; } -export function get(rawObj: object, key: string | symbol, receiver: any): any { +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; @@ -34,7 +34,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any): any { // 对于prototype不做代理 if (key !== 'prototype') { // 对于value也需要进一步代理 - const valProxy = createProxy(value, hookObserverMap.get(rawObj)); + const valProxy = singleLevel ? value : createProxy(value, hookObserverMap.get(rawObj)); return valProxy; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index ecbe22d9..e82a4a96 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -91,7 +91,7 @@ export function createStore, C extend throw new Error('store obj must be pure object'); } - const proxyObj = createProxy(config.state, !config.options?.suppressHooks); + const proxyObj = createProxy(config.state, !config.options?.reduxAdapter); proxyObj.$pending = false; diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx index 66c287e9..d7cab296 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx @@ -1,5 +1,5 @@ //@ts-ignore -import * as Horizon from '@cloudsop/horizon/index.ts'; +import * as Horizon from '../../../../libs/horizon'; import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; import { triggerClickEvent } from '../../jest/commonComponents'; import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; @@ -8,159 +8,90 @@ const { unmountComponentAtNode } = Horizon; function postpone(timer, func) { return new Promise(resolve => { - setTimeout(function() { + window.setTimeout(function () { + console.log('resolving postpone'); resolve(func()); }, timer); }); } -describe('Asynchronous functions', () => { - let container: HTMLElement | null = null; - - const COUNTER_ID = 'counter'; - const TOGGLE_ID = 'toggle'; - const TOGGLE_FAST_ID = 'toggleFast'; - const RESULT_ID = 'result'; - - let useAsyncCounter; +describe('Asynchronous store', () => { + const useAsyncCounter = createStore({ + state: { + counter: 0, + check: false, + }, + actions: { + increment: function (state) { + return new Promise(resolve => { + window.setTimeout(() => { + state.counter++; + resolve(true); + }, 10); + }); + }, + toggle: function (state) { + state.check = !state.check; + }, + reset: function (state) { + state.check = false; + state.counter = 0; + }, + }, + computed: { + value: state => { + return (state.check ? 'true' : 'false') + state.counter; + }, + }, + }); beforeEach(() => { - useAsyncCounter = createStore({ - state: { - counter: 0, - check: false, - }, - actions: { - increment: function(state) { - return new Promise(resolve => { - setTimeout(() => { - state.counter++; - resolve(true); - }, 100); - }); - }, - toggle: function(state) { - state.check = !state.check; - }, - }, - computed: { - value: state => { - return (state.check ? 'true' : 'false') + state.counter; - }, - }, - }); - container = document.createElement('div'); - document.body.appendChild(container); + useAsyncCounter().reset(); }); - afterEach(() => { - unmountComponentAtNode(container); - container?.remove(); - container = null; - }); - - it('Should wait for async actions', async () => { - // @ts-ignore - jest.useRealTimers(); - let globalStore; - - function App() { - const store = useAsyncCounter(); - globalStore = store; - - return ( -
-

{store.value}

- - - -
- ); - } - - Horizon.render(, container); - - // initial state - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); - - // slow toggle has nothing to wait for, it is resolved immediately - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_ID); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); - - // counter increment is slow. slow toggle waits for result - Horizon.act(() => { - triggerClickEvent(container, COUNTER_ID); - }); - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_ID); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true0'); - - // fast toggle does not wait for counter and it is resolved immediately - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_FAST_ID); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); - - // at 150ms counter increment will be resolved and slow toggle immediately after - const t150 = postpone(150, () => { - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('true1'); - }); - - // before that, two more actions are added to queue - another counter and slow toggle - Horizon.act(() => { - triggerClickEvent(container, COUNTER_ID); - }); - Horizon.act(() => { - triggerClickEvent(container, TOGGLE_ID); - }); - - // at 250ms they should be already resolved - const t250 = postpone(250, () => { - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false2'); - }); - - await Promise.all([t150, t250]); - }); - - it('call async action by then', async () => { - // @ts-ignore + it('should return promise when queued function is called', () => { jest.useFakeTimers(); - let globalStore; - function App() { - const store = useAsyncCounter(); - globalStore = store; + const store = useAsyncCounter(); - return ( -
-

{store.value}

-
- ); - } + return new Promise(resolve => { + store.$queue.increment().then(() => { + expect(store.counter == 1); + resolve(true); + }); - Horizon.render(, container); - - // call async action by then - globalStore.$queue.increment().then(() => { - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false1'); + jest.advanceTimersByTime(150); }); + }); - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0'); + it('should queue async functions', () => { + jest.useFakeTimers(); + return new Promise(resolve => { + const store = useAsyncCounter(); - // past 150 ms - // @ts-ignore - jest.advanceTimersByTime(150); + //initial value + expect(store.value).toBe('false0'); + + // no blocking action action + store.$queue.toggle(); + expect(store.value).toBe('true0'); + + // store is not updated before blocking action is resolved + store.$queue.increment(); + const togglePromise = store.$queue.toggle(); + expect(store.value).toBe('true0'); + + // fast action is resolved immediatelly + store.toggle(); + expect(store.value).toBe('false0'); + + // queued action waits for blocking action to resolve + togglePromise.then(() => { + expect(store.value).toBe('true1'); + resolve(); + }); + + jest.advanceTimersByTime(150); + }); }); }); diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js b/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js deleted file mode 100644 index 509706cf..00000000 --- a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterPromiseMiddleware.js +++ /dev/null @@ -1,96 +0,0 @@ -export const ActionType = { - Pending: 'PENDING', - Fulfilled: 'FULFILLED', - Rejected: 'REJECTED', -}; - -export const promise = store => next => action => { - //let result = next(action); - store._horizonXstore.$queue.dispatch(action); - return result; -}; - -export function createPromise(config = {}) { - const defaultTypes = [ActionType.Pending, ActionType.Fulfilled, ActionType.Rejected]; - const PROMISE_TYPE_SUFFIXES = config.promiseTypeSuffixes || defaultTypes; - const PROMISE_TYPE_DELIMITER = config.promiseTypeDelimiter || '_'; - - return store => { - const { dispatch } = store; - - return next => action => { - /** - * Instantiate variables to hold: - * (1) the promise - * (2) the data for optimistic updates - */ - let promise; - let data; - - /** - * There are multiple ways to dispatch a promise. The first step is to - * determine if the promise is defined: - * (a) explicitly (action.payload.promise is the promise) - * (b) implicitly (action.payload is the promise) - * (c) as an async function (returns a promise when called) - * - * If the promise is not defined in one of these three ways, we don't do - * anything and move on to the next middleware in the middleware chain. - */ - - // Step 1a: Is there a payload? - if (action.payload) { - const PAYLOAD = action.payload; - - // Step 1.1: Is the promise implicitly defined? - if (isPromise(PAYLOAD)) { - promise = PAYLOAD; - } - - // Step 1.2: Is the promise explicitly defined? - else if (isPromise(PAYLOAD.promise)) { - promise = PAYLOAD.promise; - data = PAYLOAD.data; - } - - // Step 1.3: Is the promise returned by an async function? - else if (typeof PAYLOAD === 'function' || typeof PAYLOAD.promise === 'function') { - promise = PAYLOAD.promise ? PAYLOAD.promise() : PAYLOAD(); - data = PAYLOAD.promise ? PAYLOAD.data : undefined; - - // Step 1.3.1: Is the return of action.payload a promise? - if (!isPromise(promise)) { - // If not, move on to the next middleware. - return next({ - ...action, - payload: promise, - }); - } - } - - // Step 1.4: If there's no promise, move on to the next middleware. - else { - return next(action); - } - - // Step 1b: If there's no payload, move on to the next middleware. - } else { - return next(action); - } - - /** - * Instantiate and define constants for: - * (1) the action type - * (2) the action meta - */ - const TYPE = action.type; - const META = action.meta; - - /** - * Instantiate and define constants for the action type suffixes. - * These are appended to the end of the action type. - */ - const [PENDING, FULFILLED, REJECTED] = PROMISE_TYPE_SUFFIXES; - }; - }; -} From 5d8eebf14487af249fdaab1bbf777799fca2901a Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 24 Aug 2022 17:29:09 +0800 Subject: [PATCH 04/19] Match-id-cec2ce18d7d08f5b4f5642c57aaa799038a73223 --- .../src/horizonx/store/StoreHandler.ts | 8 +- .../src/renderer/render/BaseComponent.ts | 3 + .../multipleStores.test.tsx | 186 ++++++++++++++++++ 3 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index bdbf8f77..fc3ed6c9 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -204,7 +204,8 @@ export function createStore, C extend return createStoreHook(handler); } -function clearVNodeObservers(vNode) { +export function clearVNodeObservers(vNode) { + if(!vNode.observers) return; vNode.observers.forEach(observer => { observer.clearByVNode(vNode); }); @@ -220,10 +221,7 @@ function hookStore() { return; } - if (processingVNode.observers) { - // 清除上一次缓存的Observer依赖 - clearVNodeObservers(processingVNode); - } else { + if (!processingVNode.observers) { processingVNode.observers = new Set(); } diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index 1ea097d3..46fd1e0a 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -12,6 +12,7 @@ import { FlagUtils } from '../vnode/VNodeFlags'; import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator'; import componentRenders from './index'; import {setProcessingVNode} from '../GlobalVar'; +import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; // 复用vNode时,也需对stack进行处理 function handlerContext(processing: VNode) { @@ -55,6 +56,8 @@ export function captureVNode(processing: VNode): VNode | null { processing.shouldUpdate = false; setProcessingVNode(processing); + + clearVNodeObservers(processing); const child = component.captureRender(processing, shouldUpdate); setProcessingVNode(null); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx new file mode 100644 index 00000000..a9b933d9 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx @@ -0,0 +1,186 @@ +//@ts-ignore +import Horizon, { createStore } from '@cloudsop/horizon/index.ts'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Horizon; + +const useStore1 = createStore({ + state:{ counter:1 }, + actions:{ + add:(state)=>state.counter++, + reset: (state)=>state.counter=1 + } +}) + +const useStore2 = createStore({ + state:{ counter2:1 }, + actions:{ + add2:(state)=>state.counter2++, + reset: (state)=>state.counter2=1 + } +}) + +describe('Using multiple stores', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const BUTTON_ID2 = 'btn2'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + useStore1().reset(); + useStore2().reset(); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use multiple stores in class component', () => { + class App extends Horizon.Component{ + render(){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + return ( +
+ + +

{counter*counter2}

+
+ ) + } + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID2); + triggerClickEvent(container, BUTTON_ID2); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); + }); + + it('Should use use stores in cycles and multiple methods', () => { + interface App { + store:any, + store2:any + } + class App extends Horizon.Component{ + constructor(){ + super(); + this.store = useStore1(); + this.store2 = useStore2() + } + + render(){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + for(let i=0; i<100; i++){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + } + + return ( +
+ + +

{counter*counter2}

+
+ ) + } + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID2); + triggerClickEvent(container, BUTTON_ID2); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); + }); + + it('Should use multiple stores in function component', () => { + function App() { + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + return ( +
+ + +

{counter*counter2}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID); + triggerClickEvent(container, BUTTON_ID2); + triggerClickEvent(container, BUTTON_ID2); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); + }); +}); \ No newline at end of file From b20293076caec6184cab54f64d69f18c33bda9da Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 31 Aug 2022 16:41:25 +0800 Subject: [PATCH 05/19] Match-id-0c7552eff73b3193eb58fd25a2dfe13e09f93e41 --- libs/horizon/index.ts | 3 + libs/horizon/src/horizonx/proxy/Observer.ts | 2 + .../proxy/handlers/ArrayProxyHandler.ts | 20 ++ .../proxy/handlers/CollectionProxyHandler.ts | 18 ++ .../proxy/handlers/ObjectProxyHandler.ts | 17 ++ .../src/horizonx/store/StoreHandler.ts | 2 +- .../src/renderer/render/BaseComponent.ts | 2 +- .../multipleStores.test.tsx | 186 ------------------ 8 files changed, 62 insertions(+), 188 deletions(-) delete mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 9e29e71d..f8035375 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -33,6 +33,7 @@ import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler'; import * as reduxAdapter from './src/horizonx/adapters/redux'; +import { watch } from './src/horizonx/proxy/watch'; // act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 const act = fun => { @@ -85,6 +86,7 @@ const Horizon = { useStore, clearStore, reduxAdapter, + watch }; export const version = __VERSION__; @@ -125,6 +127,7 @@ export { useStore, clearStore, reduxAdapter, + watch }; export default Horizon; diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index d93c7a05..b00e942e 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -33,6 +33,8 @@ export class Observer implements IObserver { listeners:(()=>void)[] = []; + watchers={} as {[key:string]:((key:string, oldValue:any, newValue:any)=>void)[]} + useProp(key: string | symbol): void { const processingVNode = getProcessingVNode(); if (processingVNode === null || !processingVNode.observers) { diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index a6812bde..dd3fdb0b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -12,6 +12,20 @@ export function createArrayProxy(rawObj: any[]): any[] { } 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); } @@ -29,6 +43,12 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const observer = getObserver(rawObj); if (!isSame(newValue, oldValue)) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } + observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index bd044b44..15aaa682 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -34,6 +34,18 @@ function get(rawObj: { size: number }, key: any, receiver: any): any { } 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); @@ -67,6 +79,12 @@ function set( } if (valChange) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } + observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 4dc27a0b..29ab2658 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -19,6 +19,18 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL 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); } @@ -54,6 +66,11 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo const ret = Reflect.set(rawObj, key, newValue, receiver); if (!isSame(newValue, oldValue)) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index fc3ed6c9..00d4695e 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -220,7 +220,7 @@ function hookStore() { if (!processingVNode) { return; } - + if (!processingVNode.observers) { processingVNode.observers = new Set(); } diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index 46fd1e0a..4192e100 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -57,7 +57,7 @@ export function captureVNode(processing: VNode): VNode | null { setProcessingVNode(processing); - clearVNodeObservers(processing); + if(processing.observers) clearVNodeObservers(processing); const child = component.captureRender(processing, shouldUpdate); setProcessingVNode(null); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx deleted file mode 100644 index a9b933d9..00000000 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/multipleStores.test.tsx +++ /dev/null @@ -1,186 +0,0 @@ -//@ts-ignore -import Horizon, { createStore } from '@cloudsop/horizon/index.ts'; -import { triggerClickEvent } from '../../jest/commonComponents'; -import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; - -const { unmountComponentAtNode } = Horizon; - -const useStore1 = createStore({ - state:{ counter:1 }, - actions:{ - add:(state)=>state.counter++, - reset: (state)=>state.counter=1 - } -}) - -const useStore2 = createStore({ - state:{ counter2:1 }, - actions:{ - add2:(state)=>state.counter2++, - reset: (state)=>state.counter2=1 - } -}) - -describe('Using multiple stores', () => { - let container: HTMLElement | null = null; - - const BUTTON_ID = 'btn'; - const BUTTON_ID2 = 'btn2'; - const RESULT_ID = 'result'; - - beforeEach(() => { - container = document.createElement('div'); - document.body.appendChild(container); - useStore1().reset(); - useStore2().reset(); - }); - - afterEach(() => { - unmountComponentAtNode(container); - container?.remove(); - container = null; - }); - - it('Should use multiple stores in class component', () => { - class App extends Horizon.Component{ - render(){ - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - - return ( -
- - -

{counter*counter2}

-
- ) - } - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID2); - triggerClickEvent(container, BUTTON_ID2); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); - }); - - it('Should use use stores in cycles and multiple methods', () => { - interface App { - store:any, - store2:any - } - class App extends Horizon.Component{ - constructor(){ - super(); - this.store = useStore1(); - this.store2 = useStore2() - } - - render(){ - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - - for(let i=0; i<100; i++){ - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - } - - return ( -
- - -

{counter*counter2}

-
- ) - } - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID2); - triggerClickEvent(container, BUTTON_ID2); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); - }); - - it('Should use multiple stores in function component', () => { - function App() { - const {counter,add} = useStore1(); - const store2 = useStore2(); - const {counter2, add2} = store2; - - return ( -
- - -

{counter*counter2}

-
- ); - } - - Horizon.render(, container); - - Horizon.act(() => { - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID); - triggerClickEvent(container, BUTTON_ID2); - triggerClickEvent(container, BUTTON_ID2); - }); - - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('9'); - }); -}); \ No newline at end of file From fbf7e8370ba029b60a210f68256abde29ca7afbe Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 19 Sep 2022 17:19:07 +0800 Subject: [PATCH 06/19] Match-id-0a9b956d94031f8d8f03fb90913325154133063d --- libs/horizon/src/horizonx/adapters/redux.ts | 32 +-- .../src/horizonx/adapters/reduxReact.ts | 77 ++++--- libs/horizon/src/horizonx/proxy/watch.ts | 8 + .../src/horizonx/store/StoreHandler.ts | 21 +- package.json | 5 +- .../StoreFunctionality/basicAccess.test.tsx | 54 +++-- .../StoreFunctionality/watch.test.tsx | 132 ++++++++++++ .../adapters/ReduxReactAdapter.test.tsx | 2 +- .../HorizonXText/adapters/connectTest.tsx | 46 ++++ .../edgeCases/multipleStores.test.tsx | 199 ++++++++++++++++++ 10 files changed, 506 insertions(+), 70 deletions(-) create mode 100644 libs/horizon/src/horizonx/proxy/watch.ts create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx create mode 100644 scripts/__tests__/HorizonXText/adapters/connectTest.tsx create mode 100644 scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index 4372e0fa..ed17b5d4 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -32,34 +32,34 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; -function mergeData(state,data){ - console.log('merging data',{state,data}); - if(!data){ +function mergeData(state, data) { + console.log('merging data', { state, data }); + if (!data) { console.log('!data'); - state.stateWrapper=data; + state.stateWrapper = data; return; } - if(Array.isArray(data) && Array.isArray(state?.stateWrapper)){ + if (Array.isArray(data) && Array.isArray(state?.stateWrapper)) { console.log('data is array'); state.stateWrapper.length = data.length; - data.forEach((item,idx) => { - if(item!=state.stateWrapper[idx]){ - state.stateWrapper[idx]=item; + data.forEach((item, idx) => { + if (item != state.stateWrapper[idx]) { + state.stateWrapper[idx] = item; } }); return; } - if(typeof data === 'object' && typeof state?.stateWrapper === 'object'){ + if (typeof data === 'object' && typeof state?.stateWrapper === 'object') { console.log('data is object'); Object.keys(state.stateWrapper).forEach(key => { - if(!data.hasOwnProperty(key)) delete state.stateWrapper[key]; - }) + if (!data.hasOwnProperty(key)) delete state.stateWrapper[key]; + }); - Object.entries(data).forEach(([key,value])=>{ - if(state.stateWrapper[key]!==value){ - state.stateWrapper[key]=value; + Object.entries(data).forEach(([key, value]) => { + if (state.stateWrapper[key] !== value) { + state.stateWrapper[key] = value; } }); return; @@ -100,7 +100,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): const result = { reducer, - getState: function() { + getState: function () { return store.$s.stateWrapper; }, subscribe: listener => { @@ -169,7 +169,7 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis return boundActionCreators; } -export function compose(middlewares: ReduxMiddleware[]) { +export function compose(...middlewares: ReduxMiddleware[]) { return (store: ReduxStoreHandler, extraArgument: any) => { let val; middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => { diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index 0954fbb3..09e4b081 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -4,7 +4,8 @@ import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import { BoundActionCreator } from './redux'; import { ReduxAction } from './redux'; -import { ReduxStoreHandler } from '../store/StoreHandler' +import { ReduxStoreHandler } from '../store/StoreHandler'; +import { VNode } from '../../renderer/Types'; const DefaultContext = createContext(null); type Context = typeof DefaultContext; @@ -22,15 +23,15 @@ export function Provider({ return createElement(Context.Provider, { value: store }, children); } -export function createStoreHook(context: Context) { +export function createStoreHook(context: Context): () => ReduxStoreHandler { return () => { - return useContext(context); + return useContext(context) as unknown as ReduxStoreHandler; }; } export function createSelectorHook(context: Context): (selector?: (any) => any) => any { - const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; - return function(selector = state => state) { + const store = createStoreHook(context)() as unknown as ReduxStoreHandler; + return function (selector = state => state) { const [b, fr] = useState(false); useEffect(() => { @@ -44,9 +45,9 @@ export function createSelectorHook(context: Context): (selector?: (any) => any) }; } -export function createDispatchHook(context: Context): ()=>BoundActionCreator { - const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler; - return function() { +export function createDispatchHook(context: Context): () => BoundActionCreator { + const store = createStoreHook(context)() as unknown as ReduxStoreHandler; + return function () { return action => { store.dispatch(action); }; @@ -84,28 +85,46 @@ export const useStore = () => { // areMergedPropsEqual: shallowCompare // }; -export function connect( - mapStateToProps?: (state: any, ownProps: { [key: string]: any }) => Object, - mapDispatchToProps?: - | { [key: string]: (...args: any[]) => ReduxAction } - | ((dispatch: (action: ReduxAction) => any, ownProps?: Object) => Object), - mergeProps?: (stateProps: Object, dispatchProps: Object, ownProps: Object) => Object, +type MapStateToPropsP = (state: any, ownProps: OwnProps) => StateProps; +type MapDispatchToPropsP = + | { [key: string]: (...args: any[]) => ReduxAction } + | ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps); +type MergePropsP = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnProps +) => MergedProps; + +type WrappedComponent = (props: OwnProps) => ReturnType; +type OriginalComponent = (props: MergedProps) => ReturnType; +type Connector = (Component: OriginalComponent) => WrappedComponent; + +export function connect( + mapStateToProps: MapStateToPropsP = () => ({} as StateProps), + mapDispatchToProps: MapDispatchToPropsP = () => ({} as DispatchProps), + mergeProps: MergePropsP = ( + stateProps, + dispatchProps, + ownProps + ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as MergedProps), options?: { areStatesEqual?: (oldState: any, newState: any) => boolean; - context?: any; // TODO: type this + context?: Context; } -) { +): Connector { if (!options) { options = {}; } - return Component => { + //this component should bear the type returned from mapping functions + return (Component: OriginalComponent): WrappedComponent => { const useStore = createStoreHook(options?.context || DefaultContext); - function Wrapper(props) { + //this component should mimic original type of component used + const Wrapper: WrappedComponent = (props: OwnProps) => { const [f, forceReload] = useState(true); - const store = (useStore() as unknown) as ReduxStoreHandler; + const store = useStore(); useEffect(() => { const unsubscribe = store.subscribe(() => forceReload(!f)); @@ -119,36 +138,34 @@ export function connect( mappedState: {}, }) as { current: { - state: {}; - mappedState: {}; + state: { [key: string]: any }; + mappedState: StateProps; }; }; - let mappedState; + let mappedState: StateProps; if (options?.areStatesEqual) { if (options.areStatesEqual(previous.current.state, store.getState())) { - mappedState = previous.current.mappedState; + mappedState = previous.current.mappedState as StateProps; } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {}; + mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); previous.current.mappedState = mappedState; } } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {}; + mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); previous.current.mappedState = mappedState; } - let mappedDispatch: { dispatch?: (action) => void } = {}; + let mappedDispatch: DispatchProps = {} as DispatchProps; if (mapDispatchToProps) { if (typeof mapDispatchToProps === 'object') { Object.entries(mapDispatchToProps).forEach(([key, value]) => { - mappedDispatch[key] = (...args) => { + mappedDispatch[key] = (...args: ReduxAction[]) => { store.dispatch(value(...args)); }; }); } else { mappedDispatch = mapDispatchToProps(store.dispatch, props); } - } else { - mappedDispatch.dispatch = store.dispatch; } const mergedProps = ( mergeProps || @@ -161,7 +178,7 @@ export function connect( const node = createElement(Component, mergedProps); return node; - } + }; return Wrapper; }; diff --git a/libs/horizon/src/horizonx/proxy/watch.ts b/libs/horizon/src/horizonx/proxy/watch.ts new file mode 100644 index 00000000..87f55280 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/watch.ts @@ -0,0 +1,8 @@ +export function watch(stateVariable:any,listener:(stateVariable:any)=>void){ + listener = listener.bind(null,stateVariable); + stateVariable.addListener(listener); + + return ()=>{ + stateVariable.removeListener(listener); + } +} \ No newline at end of file diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 00d4695e..f8055275 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -35,9 +35,7 @@ type StoreHandler, C extends UserComp $queue: QueuedStoreActions; $a: StoreActions; $c: UserComputedValues; -} & { [K in keyof S]: S[K] } & - { [K in keyof A]: Action } & - { [K in keyof C]: ReturnType }; +} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; type PlannedAction> = { action: string; @@ -76,6 +74,7 @@ export function createStore, C extend //create a local shalow copy to ensure consistency (if user would change the config object after store creation) config = { id: config.id, + /* @ts-ignore*/ options: config.options, state: config.state, actions: config.actions ? { ...config.actions } : undefined, @@ -87,6 +86,7 @@ export function createStore, C extend throw new Error('store obj must be pure object'); } + /* @ts-ignore*/ const proxyObj = createProxy(config.state, !config.options?.reduxAdapter); proxyObj.$pending = false; @@ -103,7 +103,7 @@ export function createStore, C extend const $a: Partial> = {}; const $queue: Partial> = {}; const $c: Partial> = {}; - const handler = ({ + const handler = { $subscribe, $unsubscribe, $a: $a as StoreActions, @@ -111,7 +111,7 @@ export function createStore, C extend $c: $c as ComputedValues, $config: config, $queue: $queue as QueuedStoreActions, - } as unknown) as StoreHandler; + } as unknown as StoreHandler; function tryNextAction() { if (!plannedActions.length) { @@ -193,6 +193,9 @@ export function createStore, C extend Object.keys(config.state).forEach(key => { Object.defineProperty(handler, key, { get: () => proxyObj[key], + set: value => { + proxyObj[key] = value; + }, }); }); } @@ -205,7 +208,7 @@ export function createStore, C extend } export function clearVNodeObservers(vNode) { - if(!vNode.observers) return; + if (!vNode.observers) return; vNode.observers.forEach(observer => { observer.clearByVNode(vNode); }); @@ -220,14 +223,14 @@ function hookStore() { if (!processingVNode) { return; } - + if (!processingVNode.observers) { processingVNode.observers = new Set(); } if (processingVNode.tag === FunctionComponent) { // from FunctionComponent - const vNodeRef = (useRef(null) as unknown) as { current: VNode }; + const vNodeRef = useRef(null) as unknown as { current: VNode }; vNodeRef.current = processingVNode; useEffect(() => { @@ -239,7 +242,7 @@ function hookStore() { } else if (processingVNode.tag === ClassComponent) { // from ClassComponent if (!processingVNode.classComponentWillUnmount) { - processingVNode.classComponentWillUnmount = function(vNode) { + processingVNode.classComponentWillUnmount = function (vNode) { clearVNodeObservers(vNode); vNode.observers = null; }; diff --git a/package.json b/package.json index 500c07f5..96429935 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ "@babel/plugin-transform-object-super": "7.16.7", "@babel/plugin-transform-parameters": "7.16.7", "@babel/plugin-transform-react-jsx": "7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/plugin-transform-runtime": "7.16.7", "@babel/plugin-transform-shorthand-properties": "7.16.7", "@babel/plugin-transform-spread": "7.16.7", "@babel/plugin-transform-template-literals": "7.16.7", - "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/preset-env": "7.16.7", "@babel/preset-typescript": "7.16.7", "@rollup/plugin-babel": "^5.3.1", @@ -66,5 +66,8 @@ "engines": { "node": ">=10.x", "npm": ">=7.x" + }, + "dependencies": { + "ejs": "^3.1.8" } } diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx index 9d95c95f..9647bb1d 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx @@ -36,6 +36,34 @@ describe('Basic store manipulation', () => { expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1'); }); + it('Should use direct setters', () => { + function App() { + const logStore = useLogStore(); + + return ( +
+ +

{logStore.logs[0]}

+
+ ); + } + + Horizon.render(, container); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + }); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('q'); + }); + it('Should use actions and update components', () => { function App() { const logStore = useLogStore(); @@ -74,7 +102,7 @@ describe('Basic store manipulation', () => { increment: state => { state.count++; }, - doublePlusOne: function(state) { + doublePlusOne: function (state) { state.count = state.count * 2; this.increment(); }, @@ -115,20 +143,20 @@ describe('Basic store manipulation', () => { count: 2, }, actions: { - doublePlusOne: function(state) { + doublePlusOne: function (state) { state.count = this.double + 1; }, }, - computed:{ - double: (state) => { - return state.count*2 - } - } + computed: { + double: state => { + return state.count * 2; + }, + }, }); - + function App() { const incrementStore = useIncrementStore(); - + return (
); } - + Horizon.render(, container); - + Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); }); - + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5'); - }) + }); }); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx new file mode 100644 index 00000000..806c6029 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx @@ -0,0 +1,132 @@ +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()=>{ + const useStore = createStore({ + state:{ + variable:'x' + }, + actions:{ + change:(state)=>state.variable = "a" + } + }); + + const store = useStore(); + let counter = 0; + + watch(store.$s,(state)=>{ + counter++; + expect(state.variable).toBe('a'); + }) + + store.change(); + + expect(counter).toBe(1); + }); + it('shouhld watch object variable', async()=>{ + const useStore = createStore({ + state:{ + variable:'x' + }, + actions:{ + change:(state)=>state.variable = "a" + } + }); + + const store = useStore(); + let counter = 0; + + store.$s.watch('variable',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('shouhld watch array item', async()=>{ + const useStore = createStore({ + state:{ + arr:['x'] + }, + actions:{ + change:(state)=>state.arr[0]='a' + } + }); + + const store = useStore(); + let counter = 0; + + store.arr.watch('0',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('shouhld watch collection item', async()=>{ + const useStore = createStore({ + state:{ + collection:new Map([ + ['a', 'a'], + ]) + }, + actions:{ + change:(state)=>state.collection.set('a','x') + } + }); + + const store = useStore(); + let counter = 0; + + store.collection.watch('a',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('should watch multiple variables independedntly', async()=>{ + const useStore = createStore({ + state:{ + bool1:true, + bool2:false + }, + actions:{ + toggle1:state=>state.bool1=!state.bool1, + toggle2:state=>state.bool2=!state.bool2 + } + }); + + let counter1=0; + let counterAll=0; + const store = useStore(); + + watch(store.$s,()=>{ + counterAll++; + }) + + store.$s.watch('bool1',()=>{ + counter1++; + }); + + store.toggle1(); + store.toggle1(); + + store.toggle2(); + + store.toggle1(); + + store.toggle2(); + store.toggle2(); + + expect(counter1).toBe(3); + expect(counterAll).toBe(6); + }) +}) \ No newline at end of file diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx index 4f6feec6..12fc9eb9 100644 --- a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx +++ b/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx @@ -293,4 +293,4 @@ describe('Redux/React binding adapter', () => { expect(getE(BUTTON).innerHTML).toBe('1'); expect(getE(BUTTON2).innerHTML).toBe('true'); }); -}); +}); \ No newline at end of file diff --git a/scripts/__tests__/HorizonXText/adapters/connectTest.tsx b/scripts/__tests__/HorizonXText/adapters/connectTest.tsx new file mode 100644 index 00000000..a11b8249 --- /dev/null +++ b/scripts/__tests__/HorizonXText/adapters/connectTest.tsx @@ -0,0 +1,46 @@ +import { createElement } from '../../../../libs/horizon/src/external/JSXElement'; +import { createDomTextVNode } from '../../../../libs/horizon/src/renderer/vnode/VNodeCreator'; +import { createStore } from '../../../../libs/horizon/src/horizonx/adapters/redux'; +import { connect } from '../../../../libs/horizon/src/horizonx/adapters/reduxReact'; + +createStore((state: number = 0, action): number => { + if (action.type === 'add') return state + 1; + return 0; +}); + +type WrappedButtonProps = { add: () => void; count: number; text: string }; + +function Button(props: WrappedButtonProps) { + const { add, count, text } = props; + return createElement( + 'button', + { + onClick: add, + }, + createDomTextVNode(text), + createDomTextVNode(': '), + createDomTextVNode(count) + ); +} + +const connector = connect( + state => ({ count: state }), + dispatch => ({ + add: (): void => { + dispatch({ type: 'add' }); + }, + }), + (stateProps, dispatchProps, ownProps: { text: string }) => ({ + add: dispatchProps.add, + count: stateProps.count, + text: ownProps.text, + }) +); + +const ConnectedButton = connector(Button); + +function App() { + return createElement('div', {}, createElement(ConnectedButton, { text: 'click' })); +} + +export default App; diff --git a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx new file mode 100644 index 00000000..63fb74e5 --- /dev/null +++ b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx @@ -0,0 +1,199 @@ +//@ts-ignore +import Horizon, { createStore } from '@cloudsop/horizon/index.ts'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Horizon; + +const useStore1 = createStore({ + state:{ counter:1 }, + actions:{ + add:(state)=>state.counter++, + reset: (state)=>state.counter=1 + } +}) + +const useStore2 = createStore({ + state:{ counter2:1 }, + actions:{ + add2:(state)=>state.counter2++, + reset: (state)=>state.counter2=1 + } +}) + +describe('Using multiple stores', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const BUTTON_ID2 = 'btn2'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + useStore1().reset(); + useStore2().reset(); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use multiple stores in class component', () => { + class App extends Horizon.Component{ + render(){ + const {counter,add} = useStore1(); + const {counter2, add2} = useStore2(); + + return ( +
+ + +

{counter} {counter2}

+
+ ) + } + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + + }); + + it('Should use use stores in cycles and multiple methods', () => { + interface App { + store:any, + store2:any + } + class App extends Horizon.Component{ + constructor(){ + super(); + this.store = useStore1(); + this.store2 = useStore2() + } + + render(){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + for(let i=0; i<100; i++){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + } + + return ( +
+ + +

{counter} {counter2}

+
+ ) + } + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + + }); + + it('Should use multiple stores in function component', () => { + function App() { + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + return ( +
+ + +

{counter} {counter2}

+
+ ); + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + }); +}); \ No newline at end of file From 5be06f068356e39e83252ed86c61588839f57066 Mon Sep 17 00:00:00 2001 From: * <*> Date: Sat, 12 Nov 2022 02:03:04 +0800 Subject: [PATCH 07/19] Match-id-efec6baa7c4396c9a4dfb7bb733134ca50f5ff34 --- libs/horizon/src/external/devtools.ts | 2 +- .../src/horizonx/devtools/constants.ts | 8 +++ libs/horizon/src/horizonx/devtools/index.ts | 51 +++++++++++++++++ .../src/horizonx/store/StoreHandler.ts | 57 ++++++++++++++++++- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 libs/horizon/src/horizonx/devtools/constants.ts create mode 100644 libs/horizon/src/horizonx/devtools/index.ts diff --git a/libs/horizon/src/external/devtools.ts b/libs/horizon/src/external/devtools.ts index 1fb1af9b..9e8295cb 100644 --- a/libs/horizon/src/external/devtools.ts +++ b/libs/horizon/src/external/devtools.ts @@ -39,7 +39,7 @@ export const helper = { return { name: HookName.RefHook, hIndex, value: (state as Ref).current }; } else if (isEffectHook(state)) { const name = - state.effectConstant == EffectConstant.LayoutEffect || (EffectConstant.LayoutEffect | EffectConstant.DepsChange) + state.effectConstant == EffectConstant.LayoutEffect || EffectConstant.LayoutEffect | EffectConstant.DepsChange ? HookName.LayoutEffectHook : HookName.EffectHook; return { name, hIndex, value: (state as Effect).effect }; diff --git a/libs/horizon/src/horizonx/devtools/constants.ts b/libs/horizon/src/horizonx/devtools/constants.ts new file mode 100644 index 00000000..df41626f --- /dev/null +++ b/libs/horizon/src/horizonx/devtools/constants.ts @@ -0,0 +1,8 @@ +export const INITIALIZED = 'horizonx store initialized'; +export const STATE_CHANGE = 'horizonx state change'; +export const SUBSCRIBED = 'horizonx subscribed'; +export const UNSUBSCRIBED = 'horizonx unsubscribed'; +export const ACTION = 'horizonx action'; +export const ACTION_QUEUED = 'horizonx action queued'; +export const QUEUE_PENDING = 'horizonx queue pending'; +export const QUEUE_FINISHED = 'horizonx queue finished'; diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts new file mode 100644 index 00000000..884daabf --- /dev/null +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -0,0 +1,51 @@ +const sessionId = Date.now(); + +function makeStoreSnapshot({ type, data }) { + const expanded = {}; + Object.keys(data.store.$c).forEach(key => { + expanded[key] = data.store[key]; + }); + data.store.expanded = expanded; + const snapshot = makeProxySnapshot({ + data, + type, + sessionId, + }); + return snapshot; +} + +function makeProxySnapshot(obj) { + let clone; + try { + if (!obj) { + return obj; + } + if (obj.nativeEvent) return obj.type + 'Event'; + if (typeof obj === 'function') { + return obj.toString(); + } + if (Array.isArray(obj)) { + clone = []; + obj.forEach(item => clone.push(makeProxySnapshot(item))); + return clone; + } else if (typeof obj === 'object') { + clone = {}; + Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value))); + return clone; + } + return obj; + } catch (err) { + throw console.log('cannot serialize object. ' + err); + } +} + +export const devtools = { + emit: (type, data) => { + console.log('store snapshot:', makeStoreSnapshot({ type, data })); + window.postMessage({ + type: 'HORIZON_DEV_TOOLS', + payload: makeStoreSnapshot({ type, data }), + from: 'dev tool hook', + }); + }, +}; diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index f8055275..af44a17f 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -6,6 +6,23 @@ import readonlyProxy from '../proxy/readonlyProxy'; import { Observer } from '../proxy/Observer'; import { FunctionComponent, ClassComponent } from '../Constants'; import { VNode } from '../../renderer/Types'; +import { devtools } from '../devtools'; +import { + ACTION, + ACTION_QUEUED, + INITIALIZED, + QUEUE_FINISHED, + STATE_CHANGE, + SUBSCRIBED, + UNSUBSCRIBED, +} from '../devtools/constants'; + +const idGenerator = { + id: 0, + get: function (prefix) { + return prefix.toString() + this.id++; + }, +}; const storeMap = new Map>(); @@ -73,7 +90,7 @@ export function createStore, C extend ): () => StoreHandler { //create a local shalow copy to ensure consistency (if user would change the config object after store creation) config = { - id: config.id, + id: config.id || idGenerator.get('UNKNOWN_STORE_'), /* @ts-ignore*/ options: config.options, state: config.state, @@ -92,10 +109,12 @@ export function createStore, C extend proxyObj.$pending = false; const $subscribe = listener => { + devtools.emit(SUBSCRIBED, { store: handler, listener }); proxyObj.addListener(listener); }; const $unsubscribe = listener => { + devtools.emit(UNSUBSCRIBED, handler); proxyObj.removeListener(listener); }; @@ -115,11 +134,13 @@ export function createStore, C extend function tryNextAction() { if (!plannedActions.length) { + devtools.emit(QUEUE_FINISHED, { store: handler }); proxyObj.$pending = false; return; } const nextAction = plannedActions.shift()!; + devtools.emit(ACTION, { store: handler, action: nextAction, fromQueue: true }); const result = config.actions ? config.actions[nextAction.action].bind(handler, proxyObj)(...nextAction.payload) : undefined; @@ -139,6 +160,14 @@ export function createStore, C extend if (config.actions) { Object.keys(config.actions).forEach(action => { ($queue as any)[action] = (...payload) => { + devtools.emit(ACTION_QUEUED, { + store: handler, + action: { + action, + payload, + }, + fromQueue: true, + }); return new Promise(resolve => { if (!proxyObj.$pending) { proxyObj.$pending = true; @@ -164,6 +193,14 @@ export function createStore, C extend }; ($a as any)[action] = function Wrapped(...payload) { + devtools.emit(ACTION, { + store: handler, + action: { + action, + payload, + }, + fromQueue: false, + }); return config.actions![action].bind(handler, proxyObj)(...payload); }; @@ -171,6 +208,14 @@ export function createStore, C extend Object.defineProperty(handler, action, { writable: false, value: (...payload) => { + devtools.emit(ACTION, { + store: handler, + action: { + action, + payload, + }, + fromQueue: false, + }); return config.actions![action].bind(handler, proxyObj)(...payload); }, }); @@ -204,6 +249,16 @@ export function createStore, C extend storeMap.set(config.id, handler); } + devtools.emit(INITIALIZED, { + store: handler, + }); + + handler.$subscribe(() => { + devtools.emit(STATE_CHANGE, { + store: handler, + }); + }); + return createStoreHook(handler); } From c0b8a5d03ae69c58b4e7805f956b0107c58e28b0 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 14 Nov 2022 18:07:24 +0800 Subject: [PATCH 08/19] Match-id-71b6384a6cb67e909c9c996382413fe86a027a06 --- libs/horizon/src/horizonx/proxy/ProxyHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index 9ea2c7af..ccc54ab1 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -29,7 +29,7 @@ export const hookObserverMap = new WeakMap(); export function createProxy(rawObj: any, isHookObserver = true): any { // 不是对象(是原始数据类型)不用代理 - if (!isObject(rawObj)) { + if (!(rawObj && isObject(rawObj))) { return rawObj; } From 3bd5d456216fe72c2506258431daf9fb26b51fa5 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 14 Nov 2022 18:37:15 +0800 Subject: [PATCH 09/19] Match-id-1f56d48f42d368d727c6a47fd2d56ee9b3603fc6 --- libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index a231988e..18ce8f1b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -70,6 +70,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { + const oldObject = JSON.stringify(rawObj); const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -89,5 +90,6 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo observer.setProp(key); } + console.log('mutation from: ', JSON.parse(oldObject), ' to: ', ret); return ret; } From ef3e42adbe1844516a7ad50ea8dabc938ca6b4ac Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 14 Nov 2022 18:45:51 +0800 Subject: [PATCH 10/19] Match-id-926c1d67d360579f81de7d630a2785652b57aabe --- libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 18ce8f1b..b2eba435 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -70,6 +70,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { + console.log('ObjectProxyHandler.set()'); const oldObject = JSON.stringify(rawObj); const observer = getObserver(rawObj); From f37a70552a1020aae89a496d3a5545bb4cf415e8 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 18 Nov 2022 22:13:06 +0800 Subject: [PATCH 11/19] Match-id-a610f3bb353c9901f46e8f676e3d6b2092a862b6 --- libs/horizon/src/horizonx/CommonUtils.ts | 91 ++++++++++++++++++- libs/horizon/src/horizonx/devtools/index.ts | 1 + .../src/horizonx/proxy/HooklessObserver.ts | 14 +-- libs/horizon/src/horizonx/proxy/Observer.ts | 22 ++--- .../proxy/handlers/ArrayProxyHandler.ts | 9 +- .../proxy/handlers/CollectionProxyHandler.ts | 17 ++-- .../proxy/handlers/ObjectProxyHandler.ts | 11 +-- .../src/horizonx/store/StoreHandler.ts | 3 +- libs/horizon/src/horizonx/types.d.ts | 26 +++--- .../StoreFunctionality/utils.test.js | 80 ++++++++++++++++ 10 files changed, 221 insertions(+), 53 deletions(-) create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js diff --git a/libs/horizon/src/horizonx/CommonUtils.ts b/libs/horizon/src/horizonx/CommonUtils.ts index e799c963..e6387b6a 100644 --- a/libs/horizon/src/horizonx/CommonUtils.ts +++ b/libs/horizon/src/horizonx/CommonUtils.ts @@ -19,19 +19,31 @@ export function isObject(obj: any): boolean { } export function isSet(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set) + ); } export function isWeakSet(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet) + ); } export function isMap(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map) + ); } export function isWeakMap(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap) + ); } export function isArray(obj: any): boolean { @@ -68,3 +80,74 @@ export function isSame(x, y) { return Object.is(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'; + if (isMap(val)) return 'map'; + if (isWeakSet(val)) return 'weakSet'; + if (isSet(val)) return 'set'; + return typeof val; +} + +export function resolveMutation(from, to) { + if (getDetailedType(from) !== getDetailedType(to)) { + return { mutation: true, from, to }; + } + + switch (getDetailedType(from)) { + case 'array': { + let len = Math.max(from.length, to.length); + const res: any[] = []; + let found = false; + for (let i = 0; i < len; i++) { + if (from.length <= i) { + res[i] = { mutation: true, to: to[i] }; + found = true; + } else if (to.length <= i) { + res[i] = { mutation: true, from: from[i] }; + found = true; + } else { + res[i] = resolveMutation(from[i], to[i]); + if (res[i].mutation) found = true; + } + } + // TODO: resolve shifts + return { mutation: found, items: res, from, to }; + } + + case 'object': { + let keys = Object.keys({ ...from, ...to }); + const res = {}; + let found = false; + keys.forEach(key => { + if (!(key in from)) { + res[key] = { mutation: true, to: to[key] }; + found = true; + return; + } + + if (!(key in to)) { + res[key] = { mutation: true, from: from[key] }; + found = true; + return; + } + res[key] = resolveMutation(from[key], to[key]); + if (res[key].mutation) found = true; + }); + return { mutation: found, attributes: res, from, to }; + } + + // TODO: implement collections + + default: { + if (from === to) return { mutation: false }; + + return { mutation: true, from, to }; + } + } +} diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 884daabf..5aecbff3 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -41,6 +41,7 @@ function makeProxySnapshot(obj) { export const devtools = { emit: (type, data) => { + if (!window['__HORIZON_DEV_HOOK__']) return; console.log('store snapshot:', makeStoreSnapshot({ type, data })); window.postMessage({ type: 'HORIZON_DEV_TOOLS', diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index ba390a3a..dd5015e9 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -19,28 +19,28 @@ import type { IObserver } from './Observer'; * 一个对象(对象、数组、集合)对应一个Observer */ export class HooklessObserver implements IObserver { - listeners: (() => void)[] = []; + listeners: ((mutation) => void)[] = []; useProp(key: string | symbol): void {} - addListener(listener: () => void) { + addListener(listener: (mutation) => void) { this.listeners.push(listener); } - removeListener(listener: () => void) { + removeListener(listener: (mutation) => void) { this.listeners = this.listeners.filter(item => item != listener); } - setProp(key: string | symbol): void { - this.triggerChangeListeners(); + setProp(key: string | symbol, mutation: any): void { + this.triggerChangeListeners(mutation); } - triggerChangeListeners(): void { + triggerChangeListeners(mutation: any): void { this.listeners.forEach(listener => { if (!listener) { return; } - listener(); + listener(mutation); }); } diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 0db792a0..2f3b15f8 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -24,9 +24,9 @@ export interface IObserver { removeListener: (listener: () => void) => void; - setProp: (key: string) => void; + setProp: (key: string, mutation: any) => void; - triggerChangeListeners: () => void; + triggerChangeListeners: (mutation: any) => void; triggerUpdate: (vNode: any) => void; @@ -43,9 +43,9 @@ export class Observer implements IObserver { keyVNodes = new Map(); - listeners: (() => void)[] = []; + listeners: ((mutation) => void)[] = []; - watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any) => void)[] }; + watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any, mutation: any) => void)[] }; // 对象的属性被使用时调用 useProp(key: string | symbol): void { @@ -76,7 +76,7 @@ export class Observer implements IObserver { } // 对象的属性被赋值时调用 - setProp(key: string | symbol): void { + setProp(key: string | symbol, mutation: any): void { const vNodes = this.keyVNodes.get(key); vNodes?.forEach((vNode: VNode) => { if (vNode.isStoreChange) { @@ -89,7 +89,7 @@ export class Observer implements IObserver { this.triggerUpdate(vNode); }); - this.triggerChangeListeners(); + this.triggerChangeListeners(mutation); } triggerUpdate(vNode: VNode): void { @@ -101,16 +101,16 @@ export class Observer implements IObserver { launchUpdateFromVNode(vNode); } - addListener(listener: () => void): void { + addListener(listener: (mutation) => void): void { this.listeners.push(listener); } - removeListener(listener: () => void): void { + removeListener(listener: (mutation) => void): void { this.listeners = this.listeners.filter(item => item != listener); } - triggerChangeListeners(): void { - this.listeners.forEach(listener => listener()); + triggerChangeListeners(mutation: any): void { + this.listeners.forEach(listener => listener(mutation)); } // 触发所有使用的props的VNode更新 @@ -118,7 +118,7 @@ export class Observer implements IObserver { const keyIt = this.keyVNodes.keys(); let keyItem = keyIt.next(); while (!keyItem.done) { - this.setProp(keyItem.value); + this.setProp(keyItem.value, {}); keyItem = keyIt.next(); } } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 8279512f..672253f8 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -16,6 +16,7 @@ import { getObserver } from '../ProxyHandler'; import { isSame, isValidIntegerKey } from '../../CommonUtils'; import { get as objectGet } from './ObjectProxyHandler'; +import { resolveMutation } from '../../CommonUtils'; export function createArrayProxy(rawObj: any[]): any[] { const handle = { @@ -53,6 +54,8 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const oldLength = rawObj.length; const newValue = value; + const oldArray = JSON.parse(JSON.stringify(rawObj)); + const ret = Reflect.set(rawObj, key, newValue, receiver); const newLength = rawObj.length; @@ -62,17 +65,17 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { // 值不一样,触发监听器 if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldArray, rawObj)); }); } // 触发属性变化 - observer.setProp(key); + observer.setProp(key, resolveMutation(oldValue, rawObj)); } if (oldLength !== newLength) { // 触发数组的大小变化 - observer.setProp('length'); + observer.setProp('length', resolveMutation(oldValue, rawObj)); } return ret; diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index dad28366..74fa57bc 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -15,6 +15,7 @@ import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { isMap, isWeakMap, isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; const COLLECTION_CHANGE = '_collectionChange'; const handler = { @@ -91,17 +92,17 @@ function set( const observer = getObserver(rawObj); if (valChange || !rawObj.has(key)) { - observer.setProp(COLLECTION_CHANGE); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldValue, rawObj)); } if (valChange) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldValue, rawObj)); }); } - observer.setProp(key); + observer.setProp(key, resolveMutation(oldValue, rawObj)); } return rawObj; @@ -109,12 +110,13 @@ function set( // Set的add方法 function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { + const oldCollection = JSON.parse(JSON.stringify(rawObj)); if (!rawObj.has(value)) { rawObj.add(value); const observer = getObserver(rawObj); - observer.setProp(value); - observer.setProp(COLLECTION_CHANGE); + observer.setProp(value, resolveMutation(oldCollection, rawObj)); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); } return rawObj; @@ -138,12 +140,13 @@ function clear(rawObj: { size: number; clear: () => void }) { } function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { + const oldCollection = JSON.parse(JSON.stringify(rawObj)); if (rawObj.has(key)) { rawObj.delete(key); const observer = getObserver(rawObj); - observer.setProp(key); - observer.setProp(COLLECTION_CHANGE); + observer.setProp(key, resolveMutation(oldCollection, rawObj)); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); return true; } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index b2eba435..9e0aac15 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import { isSame } from '../../CommonUtils'; +import { isSame, resolveMutation } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; @@ -70,8 +70,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { - console.log('ObjectProxyHandler.set()'); - const oldObject = JSON.stringify(rawObj); + const oldObject = JSON.parse(JSON.stringify(rawObj)); const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -85,12 +84,10 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo if (!isSame(newValue, oldValue)) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldObject, rawObj)); }); } - observer.setProp(key); + observer.setProp(key, resolveMutation(oldObject, rawObj)); } - - console.log('mutation from: ', JSON.parse(oldObject), ' to: ', ret); return ret; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index dec1a61a..42db626d 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -191,9 +191,10 @@ export function createStore, C extend store: storeObj, }); - storeObj.$subscribe(() => { + proxyObj.addListener(mutation => { devtools.emit(STATE_CHANGE, { store: storeObj, + mutation, }); }); diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts index f1a88e56..eec4a826 100644 --- a/libs/horizon/src/horizonx/types.d.ts +++ b/libs/horizon/src/horizonx/types.d.ts @@ -16,13 +16,13 @@ export interface IObserver { useProp: (key: string | symbol) => void; - addListener: (listener: () => void) => void; + addListener: (listener: (mutation: any) => void) => void; - removeListener: (listener: () => void) => void; + removeListener: (listener: (mutation: any) => void) => void; - setProp: (key: string | symbol) => void; + setProp: (key: string | symbol, mutation: any) => void; - triggerChangeListeners: () => void; + triggerChangeListeners: (mutation: any) => void; triggerUpdate: (vNode: any) => void; @@ -42,13 +42,13 @@ export type StoreConfig, C extends Us }; export type UserActions = { - [K: string]: ActionFunction + [K: string]: ActionFunction; }; type ActionFunction = (this: StoreObj, state: S, ...args: any[]) => any; export type StoreActions> = { - [K in keyof A]: Action + [K in keyof A]: Action; }; type Action, S extends object> = ( @@ -61,8 +61,8 @@ export type StoreObj, C extends UserC $a: StoreActions; $c: UserComputedValues; $queue: QueuedStoreActions; - $subscribe: (listener: () => void) => void; - $unsubscribe: (listener: () => void) => void; + $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 }; export type PlannedAction> = { @@ -74,11 +74,11 @@ export type PlannedAction> = { type RemoveFirstFromTuple = T['length'] extends 0 ? [] : ((...b: T) => void) extends (a, ...b: infer I) => void - ? I - : []; + ? I + : []; export type UserComputedValues = { - [K: string]: ComputedFunction + [K: string]: ComputedFunction; }; type ComputedFunction = (state: S) => any; @@ -89,9 +89,9 @@ export type AsyncAction, S extends object> = ( ) => Promise>; export type QueuedStoreActions> = { - [K in keyof A]: AsyncAction + [K in keyof A]: AsyncAction; }; export type ComputedValues> = { - [K in keyof C]: ReturnType + [K in keyof C]: ReturnType; }; diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js new file mode 100644 index 00000000..7d1f786f --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js @@ -0,0 +1,80 @@ +import { resolveMutation } from '../../../../libs/horizon/src/horizonx/CommonUtils'; + +describe('Mutation resolve', () => { + it('should resolve mutation different types', () => { + const mutation = resolveMutation(null, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(null); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, different values', () => { + const mutation = resolveMutation(13, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(13); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, same values', () => { + const mutation = resolveMutation(42, 42); + + expect(mutation.mutation).toBe(false); + expect(Object.keys(mutation).length).toBe(1); + }); + + it('should resolve mutation same type types, same objects', () => { + const mutation = resolveMutation({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, same array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, longer array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[5].mutation).toBe(true); + expect(mutation.items[5].to).toBe(6); + }); + + it('should resolve mutation same type types, shorter array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + }); + + it('should resolve mutation same type types, changed array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 'a']); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + expect(mutation.items[4].to).toBe('a'); + }); + + 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); + }); + + it('should resolve mutation same type types, changed object', () => { + const mutation = resolveMutation({ a: 1, b: 2, c: 3 }, { a: 1, c: 2 }); + + expect(mutation.mutation).toBe(true); + expect(mutation.attributes.a.mutation).toBe(false); + expect(mutation.attributes.b.mutation).toBe(true); + expect(mutation.attributes.b.from).toBe(2); + expect(mutation.attributes.c.to).toBe(2); + }); +}); From 0b11860c9d92ec06c84948f40a32252f6b0cec65 Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 30 Nov 2022 23:00:15 +0800 Subject: [PATCH 12/19] Match-id-760cbbb23b2babb6aab29450b9834f032fec4914 --- .../src/horizonx/devtools/constants.ts | 2 + libs/horizon/src/horizonx/devtools/index.ts | 69 ++++++++++++++++++- libs/horizon/src/horizonx/proxy/Observer.ts | 27 +++++++- .../src/horizonx/proxy/ProxyHandler.ts | 2 +- .../proxy/handlers/ArrayProxyHandler.ts | 11 +-- .../proxy/handlers/CollectionProxyHandler.ts | 27 +++++--- .../proxy/handlers/ObjectProxyHandler.ts | 8 ++- .../src/horizonx/store/StoreHandler.ts | 23 +++++-- 8 files changed, 140 insertions(+), 29 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/constants.ts b/libs/horizon/src/horizonx/devtools/constants.ts index df41626f..3e105125 100644 --- a/libs/horizon/src/horizonx/devtools/constants.ts +++ b/libs/horizon/src/horizonx/devtools/constants.ts @@ -6,3 +6,5 @@ export const ACTION = 'horizonx action'; export const ACTION_QUEUED = 'horizonx action queued'; export const QUEUE_PENDING = 'horizonx queue pending'; export const QUEUE_FINISHED = 'horizonx queue finished'; +export const RENDER_TRIGGERED = 'horizonx render triggered'; +export const OBSERVED_COMPONENTS = 'horizonx observed components'; diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 5aecbff3..7484fa88 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -1,5 +1,12 @@ +import { getStore, getAllStores } from '../store/StoreHandler'; +import { OBSERVED_COMPONENTS } from './constants'; + const sessionId = Date.now(); +export function isPanelActive() { + return window['__HORIZON_DEV_HOOK__']; +} + function makeStoreSnapshot({ type, data }) { const expanded = {}; Object.keys(data.store.$c).forEach(key => { @@ -40,9 +47,12 @@ function makeProxySnapshot(obj) { } export const devtools = { + getVNodeId: vNode => { + if (!isPanelActive()) return; + getVNodeId(vNode); + }, emit: (type, data) => { - if (!window['__HORIZON_DEV_HOOK__']) return; - console.log('store snapshot:', makeStoreSnapshot({ type, data })); + if (!isPanelActive()) return; window.postMessage({ type: 'HORIZON_DEV_TOOLS', payload: makeStoreSnapshot({ type, data }), @@ -50,3 +60,58 @@ export const devtools = { }); }, }; + +function getAffectedComponents() { + const allStores = getAllStores(); + const keys = Object.keys(allStores); + let res = {}; + keys.forEach(key => { + const subRes = new Set(); + const process = Array.from(allStores[key].$config.state._horizonObserver.keyVNodes.values()); + while (process.length) { + let pivot = process.shift(); + if (pivot?.tag) subRes.add(pivot); + if (pivot?.toString() === '[object Set]') Array.from(pivot).forEach(item => process.push(item)); + } + res[key] = Array.from(subRes).map(vnode => { + return { + name: vnode?.type + .toString() + .replace(/\{.*\}/gms, '{...}') + .replace('function ', ''), + nodeId: window.__HORIZON_DEV_HOOK__.getVnodeId(vnode), + }; + }); + }); + + return res; +} + +window.addEventListener('message', messageEvent => { + if (messageEvent.data.payload.type === 'horizonx request observed components') { + // get observed components + setTimeout(() => { + window.postMessage({ + type: 'HORIZON_DEV_TOOLS', + payload: { type: OBSERVED_COMPONENTS, data: getAffectedComponents() }, + from: 'dev tool hook', + }); + }, 100); + } + + if (messageEvent.data.payload.type === 'horizonx executue action') { + const data = messageEvent.data.payload.data; + const store = getStore(data.storeId); + if (!store?.[data.action]) { + } + + const action = store[data.action]; + const params = data.params; + action(...params).bind(store); + } +}); + +export function getVNodeId(vNode) { + window['__HORIZON_DEV_HOOK__'].send(); + return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); +} diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 2f3b15f8..adef63fc 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -16,6 +16,7 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { VNode } from '../../renderer/vnode/VNode'; +import { devtools } from '../devtools'; export interface IObserver { useProp: (key: string) => void; @@ -89,7 +90,8 @@ export class Observer implements IObserver { this.triggerUpdate(vNode); }); - this.triggerChangeListeners(mutation); + // NOTE: mutations are different in dev and production. + this.triggerChangeListeners({ mutation, vNodes }); } triggerUpdate(vNode: VNode): void { @@ -109,8 +111,27 @@ export class Observer implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } - triggerChangeListeners(mutation: any): void { - this.listeners.forEach(listener => listener(mutation)); + triggerChangeListeners({ mutation, vNodes }): void { + const nodesList = vNodes ? Array.from(vNodes) : []; + this.listeners.forEach(listener => + listener({ + mutation, + vNodes: nodesList.map(vNode => { + let realNode = vNode.realNode; + let searchedNode = vNode; + while (!realNode) { + searchedNode = searchedNode.child; + realNode = searchedNode.realNode; + } + return { + type: vNode?.type?.name, + id: devtools.getVNodeId(vNode), + path: vNode.path, + element: realNode?.outerHTML?.substr(0, 100), + }; + }), + }) + ); } // 触发所有使用的props的VNode更新 diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index ccc54ab1..fda6d653 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, isHookObserver = true): any { +export function createProxy(rawObj: any, id, isHookObserver = true): any { // 不是对象(是原始数据类型)不用代理 if (!(rawObj && isObject(rawObj))) { return rawObj; diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 672253f8..9e9c2845 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -17,6 +17,7 @@ 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 = { @@ -54,28 +55,30 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const oldLength = rawObj.length; const newValue = value; - const oldArray = JSON.parse(JSON.stringify(rawObj)); + 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, resolveMutation(oldArray, rawObj)); + cb(key, oldValue, newValue, mutation); }); } // 触发属性变化 - observer.setProp(key, resolveMutation(oldValue, rawObj)); + observer.setProp(key, mutation); } if (oldLength !== newLength) { // 触发数组的大小变化 - observer.setProp('length', resolveMutation(oldValue, rawObj)); + observer.setProp('length', mutation); } return ret; diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index 74fa57bc..05174cc5 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -16,6 +16,7 @@ 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 = { @@ -91,18 +92,20 @@ function set( 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, resolveMutation(oldValue, rawObj)); + observer.setProp(COLLECTION_CHANGE, mutation); } if (valChange) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue, resolveMutation(oldValue, rawObj)); + cb(key, oldValue, newValue, mutation); }); } - observer.setProp(key, resolveMutation(oldValue, rawObj)); + observer.setProp(key, mutation); } return rawObj; @@ -110,13 +113,16 @@ function set( // Set的add方法 function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { - const oldCollection = JSON.parse(JSON.stringify(rawObj)); + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; if (!rawObj.has(value)) { rawObj.add(value); const observer = getObserver(rawObj); - observer.setProp(value, resolveMutation(oldCollection, rawObj)); - observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); } return rawObj; @@ -140,13 +146,16 @@ function clear(rawObj: { size: number; clear: () => void }) { } function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { - const oldCollection = JSON.parse(JSON.stringify(rawObj)); + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; if (rawObj.has(key)) { rawObj.delete(key); const observer = getObserver(rawObj); - observer.setProp(key, resolveMutation(oldCollection, rawObj)); - observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); return true; } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 9e0aac15..80da37fd 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -16,6 +16,7 @@ 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, { @@ -70,7 +71,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { - const oldObject = JSON.parse(JSON.stringify(rawObj)); + const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -80,14 +81,15 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo 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, resolveMutation(oldObject, rawObj)); + cb(key, oldValue, newValue, mutation); }); } - observer.setProp(key, resolveMutation(oldObject, rawObj)); + observer.setProp(key, mutation); } return ret; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 42db626d..8b3aa154 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -14,7 +14,7 @@ */ import { useEffect, useRef } from '../../renderer/hooks/HookExternal'; -import { getProcessingVNode } from '../../renderer/GlobalVar'; +import { getProcessingVNode, getStartVNode } from '../../renderer/GlobalVar'; import { createProxy } from '../proxy/ProxyHandler'; import readonlyProxy from '../proxy/readonlyProxy'; import { Observer } from '../proxy/Observer'; @@ -60,7 +60,9 @@ export function createStore, C extend throw new Error('store obj must be pure object'); } - const proxyObj = createProxy(config.state, !config.options?.isReduxAdapter); + const id = config.id || idGenerator.get('UNNAMED_STORE'); + + const proxyObj = createProxy(config.state, id, !config.options?.isReduxAdapter); proxyObj.$pending = false; @@ -68,6 +70,7 @@ export function createStore, C extend const $queue: Partial> = {}; const $c: Partial> = {}; const storeObj = { + id, $s: proxyObj, $a: $a as StoreActions, $c: $c as ComputedValues, @@ -183,18 +186,16 @@ export function createStore, C extend }); } - if (config.id) { - storeMap.set(config.id, storeObj); - } + storeMap.set(id, storeObj); devtools.emit(INITIALIZED, { store: storeObj, }); - proxyObj.addListener(mutation => { + proxyObj.addListener(change => { devtools.emit(STATE_CHANGE, { store: storeObj, - mutation, + change, }); }); @@ -299,6 +300,14 @@ export function useStore, C extends U return storeObj as StoreObj; } +export function getStore(id: string) { + return storeMap.get(id); +} + +export function getAllStores() { + return Object.fromEntries(storeMap); +} + export function clearStore(id: string): void { storeMap.delete(id); } From 40160ad11b651852b4fe799c1eb04f64e0d9d70f Mon Sep 17 00:00:00 2001 From: * <*> Date: Wed, 7 Dec 2022 18:12:30 +0800 Subject: [PATCH 13/19] Match-id-e808ef3ee3be61db0d3fa07658d3e094257efd66 --- libs/horizon/src/horizonx/devtools/index.ts | 23 ++++++++++++--------- libs/horizon/src/horizonx/proxy/Observer.ts | 4 +++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 7484fa88..e7f3514e 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -3,10 +3,12 @@ import { OBSERVED_COMPONENTS } from './constants'; const sessionId = Date.now(); +// this function is used to detect devtool connection export function isPanelActive() { return window['__HORIZON_DEV_HOOK__']; } +// serializes store and creates expanded object with baked-in containing current computed values function makeStoreSnapshot({ type, data }) { const expanded = {}; Object.keys(data.store.$c).forEach(key => { @@ -21,6 +23,7 @@ function makeStoreSnapshot({ type, data }) { return snapshot; } +// safely serializes variables containing values wrapped in Proxy object function makeProxySnapshot(obj) { let clone; try { @@ -47,10 +50,13 @@ function makeProxySnapshot(obj) { } export const devtools = { + // returns vNode id from horizon devtools getVNodeId: vNode => { if (!isPanelActive()) return; - getVNodeId(vNode); + window['__HORIZON_DEV_HOOK__'].send(); // update list first + return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); }, + // sends horizonx devtool message to extension emit: (type, data) => { if (!isPanelActive()) return; window.postMessage({ @@ -61,6 +67,7 @@ export const devtools = { }, }; +// collects components that are dependant on horizonx store and their ids function getAffectedComponents() { const allStores = getAllStores(); const keys = Object.keys(allStores); @@ -87,8 +94,9 @@ function getAffectedComponents() { return res; } +// listens to messages from background window.addEventListener('message', messageEvent => { - if (messageEvent.data.payload.type === 'horizonx request observed components') { + if (messageEvent?.data?.payload?.type === 'horizonx request observed components') { // get observed components setTimeout(() => { window.postMessage({ @@ -99,19 +107,14 @@ window.addEventListener('message', messageEvent => { }, 100); } + // executes store action if (messageEvent.data.payload.type === 'horizonx executue action') { const data = messageEvent.data.payload.data; const store = getStore(data.storeId); - if (!store?.[data.action]) { - } + if (!store?.[data.action]) return; const action = store[data.action]; const params = data.params; - action(...params).bind(store); + action(...params); } }); - -export function getVNodeId(vNode) { - window['__HORIZON_DEV_HOOK__'].send(); - return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); -} diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index adef63fc..b1aa43d7 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -79,7 +79,9 @@ export class Observer implements IObserver { // 对象的属性被赋值时调用 setProp(key: string | symbol, mutation: any): void { const vNodes = this.keyVNodes.get(key); - vNodes?.forEach((vNode: VNode) => { + //NOTE: using Set directly can lead to deadlock + const vNodeArray = Array.from(vNodes || []); + vNodeArray?.forEach((vNode: VNode) => { if (vNode.isStoreChange) { // VNode已经被触发过,不再重复触发 return; From 2f7787c1cff9554d32a6ef4f92b94c3c4038b7b0 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 13 Feb 2023 14:51:35 +0800 Subject: [PATCH 14/19] Match-id-bae8261fcf2fb4e5c1748e8ce921736fe3bd9ae4 --- jest.config.js | 4 +- libs/horizon/src/horizonx/CommonUtils.ts | 28 +- libs/horizon/src/horizonx/devtools/index.ts | 108 ++++- .../src/horizonx/proxy/HooklessObserver.ts | 4 + .../src/horizonx/proxy/ProxyHandler.ts | 26 +- .../proxy/handlers copy/ArrayProxyHandler.ts | 85 ++++ .../handlers copy/CollectionProxyHandler.ts | 235 +++++++++++ .../proxy/handlers copy/ObjectProxyHandler.ts | 92 +++++ .../proxy/handlers/ArrayProxyHandler.ts | 121 ++++-- .../proxy/handlers/CollectionProxyHandler.ts | 232 +---------- .../src/horizonx/proxy/handlers/MapProxy.ts | 386 ++++++++++++++++++ .../proxy/handlers/ObjectProxyHandler.ts | 127 +++--- .../src/horizonx/proxy/handlers/SetProxy.ts | 297 ++++++++++++++ .../horizonx/proxy/handlers/WeakMapProxy.ts | 197 +++++++++ .../horizonx/proxy/handlers/WeakSetProxy.ts | 133 ++++++ .../src/horizonx/store/StoreHandler.ts | 49 ++- libs/horizon/src/horizonx/types.d.ts | 1 + .../StateManager/StateArray.test.tsx | 0 .../StateManager/StateMap.test.tsx | 6 +- .../StateManager/StateMixType.test.tsx | 0 .../StateManager/StateSet.test.tsx | 0 .../StateManager/StateWeakMap.test.tsx | 0 .../StateManager/StateWeakSet.test.tsx | 0 .../StoreFunctionality/async.test.tsx | 0 .../StoreFunctionality/basicAccess.test.tsx | 0 .../StoreFunctionality/cloneDeep.test.js | 0 .../StoreFunctionality/dollarAccess.test.tsx | 0 .../StoreFunctionality/otherCases.test.tsx | 0 .../StoreFunctionality/reset.js | 0 .../StoreFunctionality/store.ts | 0 .../StoreFunctionality/utils.test.js | 15 +- .../StoreFunctionality/watch.test.tsx | 2 +- .../adapters/ReduxAdapter.test.tsx | 0 .../adapters/ReduxAdapterThunk.test.tsx | 0 .../adapters/ReduxReactAdapter.test.tsx | 0 .../adapters/connectTest.tsx | 0 .../class/ClassException.test.tsx | 0 .../class/ClassStateArray.test.tsx | 0 .../class/ClassStateMap.test.tsx | 0 .../clear/ClassVNodeClear.test.tsx | 0 .../clear/FunctionVNodeClear.test.tsx | 0 .../edgeCases/deepVariableObserver.test.tsx | 155 +++++++ .../edgeCases/multipleStores.test.tsx | 0 .../edgeCases/proxy.test.tsx | 0 44 files changed, 1977 insertions(+), 326 deletions(-) create mode 100644 libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts create mode 100644 libs/horizon/src/horizonx/proxy/handlers copy/CollectionProxyHandler.ts create mode 100644 libs/horizon/src/horizonx/proxy/handlers copy/ObjectProxyHandler.ts create mode 100644 libs/horizon/src/horizonx/proxy/handlers/MapProxy.ts create mode 100644 libs/horizon/src/horizonx/proxy/handlers/SetProxy.ts create mode 100644 libs/horizon/src/horizonx/proxy/handlers/WeakMapProxy.ts create mode 100644 libs/horizon/src/horizonx/proxy/handlers/WeakSetProxy.ts rename scripts/__tests__/{HorizonXText => HorizonXTest}/StateManager/StateArray.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StateManager/StateMap.test.tsx (98%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StateManager/StateMixType.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StateManager/StateSet.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StateManager/StateWeakMap.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StateManager/StateWeakSet.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/async.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/basicAccess.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/cloneDeep.test.js (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/dollarAccess.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/otherCases.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/reset.js (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/store.ts (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/utils.test.js (87%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/StoreFunctionality/watch.test.tsx (98%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/adapters/ReduxAdapter.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/adapters/ReduxAdapterThunk.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/adapters/ReduxReactAdapter.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/adapters/connectTest.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/class/ClassException.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/class/ClassStateArray.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/class/ClassStateMap.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/clear/ClassVNodeClear.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/clear/FunctionVNodeClear.test.tsx (100%) create mode 100644 scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx rename scripts/__tests__/{HorizonXText => HorizonXTest}/edgeCases/multipleStores.test.tsx (100%) rename scripts/__tests__/{HorizonXText => HorizonXTest}/edgeCases/proxy.test.tsx (100%) 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 From 2293727e6ba941e1de7e1583422d13cb001bca6d Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 17 Feb 2023 17:37:23 +0800 Subject: [PATCH 15/19] Match-id-b078d7aff2ccd280f952cbc2be1dce941dbb61ab --- libs/horizon/src/horizonx/devtools/index.ts | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 192ece34..d0182afc 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -25,6 +25,7 @@ function makeStoreSnapshot({ type, data }) { return snapshot; } +<<<<<<< Updated upstream // safely serializes variables containing values wrapped in Proxy object function getType(value) { if (!value) return 'nullish'; @@ -41,6 +42,9 @@ function getType(value) { } function makeProxySnapshot(obj) { const type = getType(obj); +======= +function makeProxySnapshot(obj, visited: any[] = []) { +>>>>>>> Stashed changes let clone; try { @@ -54,6 +58,7 @@ function makeProxySnapshot(obj) { if (type === 'function') { return obj.toString(); } +<<<<<<< Updated upstream // VNODE if (type === 'vnode') { return { @@ -93,14 +98,23 @@ function makeProxySnapshot(obj) { } // ARRAY if (type === 'array') { +======= + if (Array.isArray(obj)) { + if (visited.some(item => item === obj)) return ``; +>>>>>>> Stashed changes clone = []; - obj.forEach(item => clone.push(makeProxySnapshot(item))); + obj.forEach(item => clone.push(makeProxySnapshot(item, visited.concat([obj])))); return clone; +<<<<<<< Updated upstream } // OBJECT if (type === 'object') { +======= + } else if (typeof obj === 'object') { + if (visited.some(item => item === obj)) return ``; +>>>>>>> Stashed changes clone = {}; - Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value))); + Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value, visited.concat([obj])))); return clone; } // PRIMITIVE @@ -134,6 +148,10 @@ function getAffectedComponents() { const keys = Object.keys(allStores); let res = {}; keys.forEach(key => { + if (!allStores[key].$config.state._horizonObserver.keyVNodes) { + res[key] = []; + return; + } const subRes = new Set(); const process = Array.from(allStores[key].$config.state._horizonObserver.keyVNodes.values()); while (process.length) { From 20694046d636925059736e1c5e374578f37d187c Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 17 Feb 2023 18:17:06 +0800 Subject: [PATCH 16/19] Match-id-e1124faaed58084937cabc0567c12f8c01af6016 --- libs/horizon/src/horizonx/devtools/index.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index d0182afc..73ab8b45 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -25,7 +25,6 @@ function makeStoreSnapshot({ type, data }) { return snapshot; } -<<<<<<< Updated upstream // safely serializes variables containing values wrapped in Proxy object function getType(value) { if (!value) return 'nullish'; @@ -40,11 +39,8 @@ function getType(value) { if (typeof value === 'object') return 'object'; return 'primitive'; } -function makeProxySnapshot(obj) { - const type = getType(obj); -======= function makeProxySnapshot(obj, visited: any[] = []) { ->>>>>>> Stashed changes + const type = getType(obj); let clone; try { @@ -58,7 +54,6 @@ function makeProxySnapshot(obj, visited: any[] = []) { if (type === 'function') { return obj.toString(); } -<<<<<<< Updated upstream // VNODE if (type === 'vnode') { return { @@ -98,21 +93,14 @@ function makeProxySnapshot(obj, visited: any[] = []) { } // ARRAY if (type === 'array') { -======= - if (Array.isArray(obj)) { if (visited.some(item => item === obj)) return ``; ->>>>>>> Stashed changes clone = []; obj.forEach(item => clone.push(makeProxySnapshot(item, visited.concat([obj])))); return clone; -<<<<<<< Updated upstream } // OBJECT if (type === 'object') { -======= - } else if (typeof obj === 'object') { if (visited.some(item => item === obj)) return ``; ->>>>>>> Stashed changes clone = {}; Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value, visited.concat([obj])))); return clone; From cdedd81f6ba544fedf1c961d865ba37b7e2ef3db Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 17 Feb 2023 18:20:18 +0800 Subject: [PATCH 17/19] Match-id-968a747b74974ffdecf73d9500293487f6bb2592 --- libs/horizon/src/horizonx/devtools/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 25428623..11818bdc 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -175,11 +175,7 @@ window.addEventListener('message', messageEvent => { } // executes store action -<<<<<<< HEAD - if (messageEvent.data.payload.type === 'horizonx executue action') { -======= if (messageEvent.data?.payload?.type === 'horizonx executue action') { ->>>>>>> master const data = messageEvent.data.payload.data; const store = getStore(data.storeId); if (!store?.[data.action]) return; From c7daceca48345eca18ab35a5d0196098f38e419c Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 17 Feb 2023 19:40:21 +0800 Subject: [PATCH 18/19] Match-id-6f25fe3657489f320737a4456ef62fb9abe367af --- libs/horizon/src/horizonx/devtools/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 11818bdc..7c14a438 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -186,7 +186,7 @@ window.addEventListener('message', messageEvent => { } // queues store action - if (messageEvent.data.payload.type === 'horizonx queue 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; @@ -197,7 +197,7 @@ window.addEventListener('message', messageEvent => { } // queues change store state - if (messageEvent.data.payload.type === 'horizonx change state') { + if (messageEvent?.data?.payload?.type === 'horizonx change state') { const data = messageEvent.data.payload; const store = getStore(data.storeId); if (!store) return; From bba1a7a7db3efda81597f8d6d6088bcce093d378 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 17 Feb 2023 21:24:21 +0800 Subject: [PATCH 19/19] Match-id-801667129e07b45fdef4e1690678d5b07c75551e --- .../proxy/handlers copy/ArrayProxyHandler.ts | 85 ------- .../handlers copy/CollectionProxyHandler.ts | 235 ------------------ .../proxy/handlers copy/ObjectProxyHandler.ts | 92 ------- 3 files changed, 412 deletions(-) delete mode 100644 libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts delete mode 100644 libs/horizon/src/horizonx/proxy/handlers copy/CollectionProxyHandler.ts delete mode 100644 libs/horizon/src/horizonx/proxy/handlers copy/ObjectProxyHandler.ts diff --git a/libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts deleted file mode 100644 index 9e9c2845..00000000 --- a/libs/horizon/src/horizonx/proxy/handlers copy/ArrayProxyHandler.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 deleted file mode 100644 index 05174cc5..00000000 --- a/libs/horizon/src/horizonx/proxy/handlers copy/CollectionProxyHandler.ts +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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 deleted file mode 100644 index 721be056..00000000 --- a/libs/horizon/src/horizonx/proxy/handlers copy/ObjectProxyHandler.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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; -}