Match-id-f14dd4c421a2527b83579268a6c6f27bddc91913

This commit is contained in:
* 2022-12-19 11:37:31 +08:00 committed by *
commit 919ace19a7
16 changed files with 565 additions and 89 deletions

View File

@ -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 };
}
}
}

View File

@ -34,9 +34,9 @@ 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;
};
}
@ -85,17 +85,19 @@ export function connect(
mergeProps?: (stateProps: object, dispatchProps: object, ownProps: object) => object,
options?: {
areStatesEqual?: (oldState: any, newState: any) => boolean;
context?: any; // TODO: type this
context?: Context;
}
) {
): Connector<OwnProps, MergedProps> {
if (!options) {
options = {};
}
return Component => {
//this component should bear the type returned from mapping functions
return (Component: OriginalComponent<MergedProps>): WrappedComponent<OwnProps> => {
const useStore = createStoreHook(options?.context || DefaultContext);
function Wrapper(props) {
//this component should mimic original type of component used
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
const [f, forceReload] = useState(true);
const store = useStore() as unknown as ReduxStoreHandler;
@ -112,36 +114,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 ||
@ -154,7 +154,7 @@ export function connect(
const node = createElement(Component, mergedProps);
return node;
}
};
return Wrapper;
};

View File

@ -0,0 +1,10 @@
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';
export const RENDER_TRIGGERED = 'horizonx render triggered';
export const OBSERVED_COMPONENTS = 'horizonx observed components';

View File

@ -0,0 +1,120 @@
import { getStore, getAllStores } from '../store/StoreHandler';
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 => {
expanded[key] = data.store[key];
});
data.store.expanded = expanded;
const snapshot = makeProxySnapshot({
data,
type,
sessionId,
});
return snapshot;
}
// safely serializes variables containing values wrapped in Proxy object
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 = {
// returns vNode id from horizon devtools
getVNodeId: vNode => {
if (!isPanelActive()) return;
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({
type: 'HORIZON_DEV_TOOLS',
payload: makeStoreSnapshot({ type, data }),
from: 'dev tool hook',
});
},
};
// collects components that are dependant on horizonx store and their ids
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;
}
// listens to messages from background
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);
}
// 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]) return;
const action = store[data.action];
const params = data.params;
action(...params);
}
});

View File

@ -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);
});
}

View File

@ -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;
@ -24,9 +25,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 +44,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 +77,7 @@ export class Observer implements IObserver {
}
// 对象的属性被赋值时调用
setProp(key: string | symbol): void {
setProp(key: string | symbol, mutation: any): void {
const vNodes = this.keyVNodes.get(key);
//NOTE: using Set directly can lead to deadlock
const vNodeArray = Array.from(vNodes || []);
@ -91,7 +92,8 @@ export class Observer implements IObserver {
this.triggerUpdate(vNode);
});
this.triggerChangeListeners();
// NOTE: mutations are different in dev and production.
this.triggerChangeListeners({ mutation, vNodes });
}
triggerUpdate(vNode: VNode): void {
@ -103,16 +105,35 @@ 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, 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更新
@ -120,7 +141,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();
}
}

View File

@ -27,9 +27,9 @@ 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 (!isObject(rawObj)) {
if (!(rawObj && isObject(rawObj))) {
return rawObj;
}

View File

@ -16,6 +16,8 @@
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 = {
@ -53,26 +55,30 @@ function set(rawObj: any[], key: string, value: any, receiver: any) {
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);
cb(key, oldValue, newValue, mutation);
});
}
// 触发属性变化
observer.setProp(key);
observer.setProp(key, mutation);
}
if (oldLength !== newLength) {
// 触发数组的大小变化
observer.setProp('length');
observer.setProp('length', mutation);
}
return ret;

View File

@ -15,6 +15,8 @@
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 = {
@ -90,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);
observer.setProp(COLLECTION_CHANGE, mutation);
}
if (valChange) {
if (observer.watchers?.[key]) {
observer.watchers[key].forEach(cb => {
cb(key, oldValue, newValue);
cb(key, oldValue, newValue, mutation);
});
}
observer.setProp(key);
observer.setProp(key, mutation);
}
return rawObj;
@ -109,12 +113,16 @@ function set(
// 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);
observer.setProp(value);
observer.setProp(COLLECTION_CHANGE);
const mutation = isPanelActive()
? resolveMutation(oldCollection, rawObj)
: { mutation: true, from: null, to: rawObj };
observer.setProp(value, mutation);
observer.setProp(COLLECTION_CHANGE, mutation);
}
return rawObj;
@ -138,12 +146,16 @@ function clear(rawObj: { size: number; clear: () => void }) {
}
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);
observer.setProp(key);
observer.setProp(COLLECTION_CHANGE);
const mutation = isPanelActive()
? resolveMutation(oldCollection, rawObj)
: { mutation: true, from: null, to: rawObj };
observer.setProp(key, mutation);
observer.setProp(COLLECTION_CHANGE, mutation);
return true;
}

View File

@ -13,9 +13,10 @@
* 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';
import { isPanelActive } from '../../devtools';
export function createObjectProxy<T extends object>(rawObj: T, singleLevel = false): ProxyHandler<T> {
const proxy = new Proxy(rawObj, {
@ -70,6 +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 = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
const observer = getObserver(rawObj);
if (value && key == 'removeListener') {
@ -79,15 +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);
cb(key, oldValue, newValue, mutation);
});
}
observer.setProp(key);
observer.setProp(key, mutation);
}
return ret;
}

View File

@ -14,26 +14,44 @@
*/
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';
import { FunctionComponent, ClassComponent } from '../../renderer/vnode/VNodeTags';
import { isPromise } from '../CommonUtils';
import type {
ActionFunction, ComputedValues,
PlannedAction, QueuedStoreActions,
ActionFunction,
ComputedValues,
PlannedAction,
QueuedStoreActions,
StoreActions,
StoreConfig,
StoreObj,
UserActions,
UserComputedValues
UserComputedValues,
} from '../types';
import { VNode } from '../../renderer/vnode/VNode';
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<string, StoreObj<any, any, any>>();
export function createStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
config: StoreConfig<S, A, C>
): () => StoreObj<S, A, C> {
@ -42,7 +60,9 @@ export function createStore<S extends object, A extends UserActions<S>, 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;
@ -50,15 +70,18 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
const $queue: Partial<StoreActions<S, A>> = {};
const $c: Partial<ComputedValues<S, C>> = {};
const storeObj = {
id,
$s: proxyObj,
$a: $a as StoreActions<S, A>,
$c: $c as ComputedValues<S, C>,
$queue: $queue as QueuedStoreActions<S, A>,
$config: config,
$subscribe: listener => {
devtools.emit(SUBSCRIBED, { store: storeObj, listener });
proxyObj.addListener(listener);
},
$unsubscribe: listener => {
devtools.emit(UNSUBSCRIBED, storeObj);
proxyObj.removeListener(listener);
},
} as unknown as StoreObj<S, A, C>;
@ -71,6 +94,14 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
// 让store.$queue[action]可以访问到action方法
// 要达到的效果如果通过store.$queue[action1]调用的action1返回promise,会阻塞下一个store.$queue[action2]
($queue as any)[action] = (...payload) => {
devtools.emit(ACTION_QUEUED, {
store: storeObj,
action: {
action,
payload,
},
fromQueue: true,
});
return new Promise(resolve => {
if (!proxyObj.$pending) {
proxyObj.$pending = true;
@ -99,6 +130,14 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
// 让store.$a[action]可以访问到action方法
($a as any)[action] = function Wrapped(...payload) {
devtools.emit(ACTION, {
store: storeObj,
action: {
action,
payload,
},
fromQueue: false,
});
return config.actions![action].bind(storeObj, proxyObj)(...payload);
};
@ -106,6 +145,14 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
Object.defineProperty(storeObj, action, {
writable: false,
value: (...payload) => {
devtools.emit(ACTION, {
store: storeObj,
action: {
action,
payload,
},
fromQueue: false,
});
return config.actions![action].bind(storeObj, proxyObj)(...payload);
},
});
@ -132,13 +179,25 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
// 从Proxy对象获取值会触发代理
return proxyObj[key];
},
set: value => {
proxyObj[key] = value;
},
});
});
}
if (config.id) {
storeMap.set(config.id, storeObj);
}
storeMap.set(id, storeObj);
devtools.emit(INITIALIZED, {
store: storeObj,
});
proxyObj.addListener(change => {
devtools.emit(STATE_CHANGE, {
store: storeObj,
change,
});
});
return createGetStore(storeObj);
}
@ -217,9 +276,10 @@ function registerDestroyFunction() {
vNodeRef.current.observers = null;
};
}, []);
} else if (processingVNode.tag === ClassComponent) { // 类组件
} else if (processingVNode.tag === ClassComponent) {
// 类组件
if (!processingVNode.classComponentWillUnmount) {
processingVNode.classComponentWillUnmount = (vNode) => {
processingVNode.classComponentWillUnmount = vNode => {
clearVNodeObservers(vNode);
vNode.observers = null;
};
@ -240,6 +300,14 @@ export function useStore<S extends object, A extends UserActions<S>, C extends U
return storeObj as StoreObj<S, A, C>;
}
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);
}

View File

@ -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<S extends object, A extends UserActions<S>, C extends Us
};
export type UserActions<S extends object> = {
[K: string]: ActionFunction<S>
[K: string]: ActionFunction<S>;
};
type ActionFunction<S extends object> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any;
export type StoreActions<S extends object, A extends UserActions<S>> = {
[K in keyof A]: Action<A[K], S>
[K in keyof A]: Action<A[K], S>;
};
type Action<T extends ActionFunction<any>, S extends object> = (
@ -61,8 +61,8 @@ export type StoreObj<S extends object, A extends UserActions<S>, C extends UserC
$a: StoreActions<S, A>;
$c: UserComputedValues<S>;
$queue: QueuedStoreActions<S, A>;
$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<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
export type PlannedAction<S extends object, F extends ActionFunction<S>> = {
@ -78,7 +78,7 @@ type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0
: [];
export type UserComputedValues<S extends object> = {
[K: string]: ComputedFunction<S>
[K: string]: ComputedFunction<S>;
};
type ComputedFunction<S extends object> = (state: S) => any;
@ -89,9 +89,9 @@ export type AsyncAction<T extends ActionFunction<any>, S extends object> = (
) => Promise<ReturnType<T>>;
export type QueuedStoreActions<S extends object, A extends UserActions<S>> = {
[K in keyof A]: AsyncAction<A[K], S>
[K in keyof A]: AsyncAction<A[K], S>;
};
export type ComputedValues<S extends object, C extends UserComputedValues<S>> = {
[K in keyof C]: ReturnType<C[K]>
[K in keyof C]: ReturnType<C[K]>;
};

View File

@ -51,6 +51,34 @@ describe('Basic store manipulation', () => {
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1');
});
it('Should use direct setters', () => {
function App() {
const logStore = useLogStore();
return (
<div>
<button
id={BUTTON_ID}
onClick={() => {
logStore.logs = ['q'];
}}
>
add
</button>
<p id={RESULT_ID}>{logStore.logs[0]}</p>
</div>
);
}
Horizon.render(<App />, 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();
@ -135,10 +163,10 @@ describe('Basic store manipulation', () => {
},
},
computed: {
double: (state) => {
return state.count*2
}
}
double: state => {
return state.count * 2;
},
},
});
function App() {
@ -166,5 +194,5 @@ describe('Basic store manipulation', () => {
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5');
})
});
});

View File

@ -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);
});
});

View File

@ -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;