Match-id-5f3ff7a11a13ae24c88d6483333762208df08e28
This commit is contained in:
parent
8995659ea5
commit
ccf66f1653
|
@ -32,6 +32,43 @@ export type ReduxMiddleware = (
|
||||||
|
|
||||||
type Reducer = (state: any, action: ReduxAction) => any;
|
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 {
|
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler {
|
||||||
const store = createStoreX({
|
const store = createStoreX({
|
||||||
id: 'defaultStore',
|
id: 'defaultStore',
|
||||||
|
@ -48,14 +85,19 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?):
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
return;
|
return;
|
||||||
} // NOTE: reducer should never return undefined, in this case, do not change state
|
} // NOTE: reducer should never return undefined, in this case, do not change state
|
||||||
|
// mergeData(state,result);
|
||||||
state.stateWrapper = result;
|
state.stateWrapper = result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
suppressHooks: true,
|
reduxAdapter: true,
|
||||||
},
|
},
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// store.$subscribe(()=>{
|
||||||
|
// console.log('changed');
|
||||||
|
// });
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
reducer,
|
reducer,
|
||||||
getState: function() {
|
getState: function() {
|
||||||
|
|
|
@ -38,7 +38,9 @@ export function createProxy(rawObj: any, hookObserver = true): any {
|
||||||
|
|
||||||
// 创建Proxy
|
// 创建Proxy
|
||||||
let proxyObj;
|
let proxyObj;
|
||||||
if (isArray(rawObj)) {
|
if (!hookObserver) {
|
||||||
|
proxyObj = createObjectProxy(rawObj,true);
|
||||||
|
} else if (isArray(rawObj)) {
|
||||||
// 数组
|
// 数组
|
||||||
proxyObj = createArrayProxy(rawObj as []);
|
proxyObj = createArrayProxy(rawObj as []);
|
||||||
} else if (isCollection(rawObj)) {
|
} else if (isCollection(rawObj)) {
|
||||||
|
|
|
@ -2,16 +2,16 @@ import { isSame } from '../../CommonUtils';
|
||||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||||
import { OBSERVER_KEY } from '../../Constants';
|
import { OBSERVER_KEY } from '../../Constants';
|
||||||
|
|
||||||
export function createObjectProxy<T extends object>(rawObj: T): ProxyHandler<T> {
|
export function createObjectProxy<T extends object>(rawObj: T, singleLevel = false): ProxyHandler<T> {
|
||||||
const proxy = new Proxy(rawObj, {
|
const proxy = new Proxy(rawObj, {
|
||||||
get,
|
get: (...args) => get(...args, singleLevel),
|
||||||
set,
|
set,
|
||||||
});
|
});
|
||||||
|
|
||||||
return proxy;
|
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.
|
// The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep.
|
||||||
if (key === OBSERVER_KEY) {
|
if (key === OBSERVER_KEY) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -34,7 +34,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any): any {
|
||||||
// 对于prototype不做代理
|
// 对于prototype不做代理
|
||||||
if (key !== 'prototype') {
|
if (key !== 'prototype') {
|
||||||
// 对于value也需要进一步代理
|
// 对于value也需要进一步代理
|
||||||
const valProxy = createProxy(value, hookObserverMap.get(rawObj));
|
const valProxy = singleLevel ? value : createProxy(value, hookObserverMap.get(rawObj));
|
||||||
|
|
||||||
return valProxy;
|
return valProxy;
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
|
||||||
throw new Error('store obj must be pure object');
|
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;
|
proxyObj.$pending = false;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//@ts-ignore
|
//@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 { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||||
import { triggerClickEvent } from '../../jest/commonComponents';
|
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||||
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||||
|
@ -8,40 +8,35 @@ const { unmountComponentAtNode } = Horizon;
|
||||||
|
|
||||||
function postpone(timer, func) {
|
function postpone(timer, func) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(function() {
|
window.setTimeout(function () {
|
||||||
|
console.log('resolving postpone');
|
||||||
resolve(func());
|
resolve(func());
|
||||||
}, timer);
|
}, timer);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Asynchronous functions', () => {
|
describe('Asynchronous store', () => {
|
||||||
let container: HTMLElement | null = null;
|
const useAsyncCounter = createStore({
|
||||||
|
|
||||||
const COUNTER_ID = 'counter';
|
|
||||||
const TOGGLE_ID = 'toggle';
|
|
||||||
const TOGGLE_FAST_ID = 'toggleFast';
|
|
||||||
const RESULT_ID = 'result';
|
|
||||||
|
|
||||||
let useAsyncCounter;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useAsyncCounter = createStore({
|
|
||||||
state: {
|
state: {
|
||||||
counter: 0,
|
counter: 0,
|
||||||
check: false,
|
check: false,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
increment: function(state) {
|
increment: function (state) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
state.counter++;
|
state.counter++;
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}, 100);
|
}, 10);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggle: function(state) {
|
toggle: function (state) {
|
||||||
state.check = !state.check;
|
state.check = !state.check;
|
||||||
},
|
},
|
||||||
|
reset: function (state) {
|
||||||
|
state.check = false;
|
||||||
|
state.counter = 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
value: state => {
|
value: state => {
|
||||||
|
@ -49,118 +44,54 @@ describe('Asynchronous functions', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
beforeEach(() => {
|
||||||
|
useAsyncCounter().reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
it('should return promise when queued function is called', () => {
|
||||||
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 (
|
|
||||||
<div>
|
|
||||||
<p id={RESULT_ID}>{store.value}</p>
|
|
||||||
<button onClick={store.$queue.increment} id={COUNTER_ID}>
|
|
||||||
add 1
|
|
||||||
</button>
|
|
||||||
<button onClick={store.$queue.toggle} id={TOGGLE_ID}>
|
|
||||||
slow toggle
|
|
||||||
</button>
|
|
||||||
<button onClick={store.toggle} id={TOGGLE_FAST_ID}>
|
|
||||||
fast toggle
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Horizon.render(<App />, 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
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
let globalStore;
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const store = useAsyncCounter();
|
const store = useAsyncCounter();
|
||||||
globalStore = store;
|
|
||||||
|
|
||||||
return (
|
return new Promise(resolve => {
|
||||||
<div>
|
store.$queue.increment().then(() => {
|
||||||
<p id={RESULT_ID}>{store.value}</p>
|
expect(store.counter == 1);
|
||||||
</div>
|
resolve(true);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Horizon.render(<App />, container);
|
|
||||||
|
|
||||||
// call async action by then
|
|
||||||
globalStore.$queue.increment().then(() => {
|
|
||||||
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('false0');
|
|
||||||
|
|
||||||
// past 150 ms
|
|
||||||
// @ts-ignore
|
|
||||||
jest.advanceTimersByTime(150);
|
jest.advanceTimersByTime(150);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should queue async functions', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const store = useAsyncCounter();
|
||||||
|
|
||||||
|
//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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
Loading…
Reference in New Issue