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