From ccf66f1653c4aee2497db67ca579ab804f1d2593 Mon Sep 17 00:00:00 2001 From: * <*> Date: Mon, 15 Aug 2022 20:47:47 +0800 Subject: [PATCH] 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; - }; - }; -}