// need to remove types: ./src/index before running `yarn build` command
///
import produce, { enableES5 } from 'immer'
enableES5()
import * as React from 'react'
import {
PureComponent,
useLayoutEffect,
useEffect,
useState,
useRef
} from 'react'
import Global from './global'
import {
Consumer,
consumerActions,
get,
getInitialState,
GlobalContext
} from './helper'
import { actionMiddlewares, applyMiddlewares, middlewares } from './middlewares'
const useStoreEffect =
typeof window === 'undefined' ? useEffect : useLayoutEffect
const isModelType = (input: any): input is ModelType => {
return (input as ModelType).state !== undefined
}
const isAPI = (input: any): input is API => {
return (input as API).useStore !== undefined
}
// useModel rules:
// DON'T USE useModel OUTSIDE createStore func
function useModel(
state: S | (() => S)
): [S, (state: Partial | ((state: S) => S | void)) => void] {
const storeId = Global.currentStoreId
const index = Global.mutableState[storeId].count
Global.mutableState[storeId].count += 1
if (!Global.mutableState[storeId].hasOwnProperty(index)) {
if (typeof state === 'function') {
// @ts-ignore
Global.mutableState[storeId][index] = state()
} else {
Global.mutableState[storeId][index] = state
}
}
const setter = async (state: Partial | ((prevState: S) => S | void)) => {
if (typeof state === 'function') {
Global.mutableState[storeId][index] = produce(
Global.mutableState[storeId][index],
// @ts-ignore
state
)
} else {
if (
Global.mutableState[storeId][index] &&
state &&
Global.mutableState[storeId][index].constructor.name === 'Object' &&
state.constructor.name === 'Object'
) {
Global.mutableState[storeId][index] = {
...Global.mutableState[storeId][index],
...state
}
} else {
Global.mutableState[storeId][index] = state
}
}
const context: InnerContext = {
Global,
action: () => {
return typeof state === 'function'
? // @ts-ignore
Global.mutableState[storeId][index]
: state
},
actionName: 'setter',
consumerActions,
disableSelectorUpdate: true,
middlewareConfig: {},
modelName: storeId,
newState: {},
params: undefined,
type: 'u'
}
// pass update event to other components subscribe the same store
return await applyMiddlewares(actionMiddlewares, context)
}
return [Global.mutableState[storeId][index], setter]
}
function createStore(useHook: CustomModelHook): LaneAPI
function createStore(name: string, useHook: CustomModelHook): LaneAPI
function createStore(n: any, u?: any): LaneAPI {
const hasName = typeof n === 'string'
Global.storeId += hasName ? 0 : 1
const storeId = hasName ? n : Global.storeId.toString()
if (!Global.Actions[storeId]) {
Global.Actions[storeId] = {}
}
if (!Global.mutableState[storeId]) {
Global.mutableState[storeId] = { count: 0 }
}
// Global.currentStoreId = storeId
// const state = useHook()
// Global.State = produce(Global.State, (s) => {
// s[hash] = state
// })
const selector = () => {
Global.mutableState[storeId].count = 0
Global.currentStoreId = storeId
Global.mutableState[storeId].cachedResult = u ? u() : n()
return Global.mutableState[storeId].cachedResult
}
Global.mutableState[storeId].selector = selector
return {
// TODO: support selector
useStore: () => useStore(storeId, selector),
getState: () => selector(),
getStore: () => Global.mutableState[storeId].cachedResult,
subscribe: (callback: () => void) => {
if (!Global.subscriptions[storeId]) {
Global.subscriptions[storeId] = []
}
Global.subscriptions[storeId].push(callback)
},
unsubscribe: (callback?: () => void) => {
if (Global.subscriptions[storeId]) {
if (callback) {
const idx = Global.subscriptions[storeId].indexOf(callback)
if (idx >= 0) Global.subscriptions[storeId].splice(idx, 1)
}
}
}
}
}
function Model>(
models: MT,
// initialState represent extContext here
initialState?: E
): API
function Model(
models: M,
initialState?: Global['State']
): APIs
function Model(
models: M | MT,
initialState?: Global['State'],
extContext?: E
) {
if (isModelType(models)) {
Global.storeId += 1
const hash = '__' + Global.storeId
Global.State = produce(Global.State, (s) => {
s[hash] = models.state
})
if (models.middlewares) {
Global.Middlewares[hash] = models.middlewares
}
Global.Actions[hash] = models.actions
Global.AsyncState[hash] = models.asyncState
// initialState represent extContext here
initialState && (Global.Context[hash] = initialState)
const actions = getActions(hash)
return {
__id: hash,
actions,
getState: () => getState(hash),
subscribe: (
actionName: keyof MT['actions'] | Array,
callback: () => void
) => subscribe(hash, actionName as string | string[], callback),
unsubscribe: (
actionName: keyof MT['actions'] | Array
) => unsubscribe(hash, actionName as string | string[]),
useStore: (selector?: Function) => useStore(hash, selector)
}
} else {
if (initialState) {
// TODO: support multi model group under SSR
Global.gid = 1
} else {
Global.gid += 1
}
let prefix = ''
if (Global.gid > 1) {
prefix = Global.gid + '_'
}
if (models.actions) {
console.error('invalid model(s) schema: ', models)
const errorFn =
(fakeReturnVal?: unknown) =>
(..._: unknown[]) => {
return fakeReturnVal
}
// Fallback Functions
return {
__ERROR__: true,
actions: errorFn({}),
getActions: errorFn({}),
getInitialState: errorFn({}),
getState: errorFn({}),
subscribe: errorFn(),
unsubscribe: errorFn(),
useStore: errorFn([{}, {}])
} as any
}
if (initialState && !initialState.__FROM_SERVER__) {
Global.State = produce(Global.State, (s) => {
Object.assign(s, initialState || {})
})
}
extContext && (Global.Context['__global'] = extContext)
let actions: { [name: string]: any } = {}
Object.keys(models).forEach((n) => {
let name = prefix + n
const model = models[n]
if (model.__ERROR__) {
// Fallback State and Actions when model schema is invalid
console.error(name + " model's schema is invalid")
Global.State = produce(Global.State, (s) => {
s[name] = {}
})
Global.Actions[name] = {}
return
}
if (!isAPI(model)) {
if (initialState && initialState.__FROM_SERVER__) {
Global.State = produce(Global.State, (s) => {
s[name] = { ...model.state, ...initialState[name] }
})
} else if (!Global.State[name]) {
Global.State = produce(Global.State, (s) => {
s[name] = model.state
})
}
if (model.middlewares) {
Global.Middlewares[name] = model.middlewares
}
Global.Actions[name] = model.actions
Global.AsyncState[name] = model.asyncState
} else {
// If you develop on SSR mode, hot reload will still keep the old Global reference, so initialState won't change unless you restart the dev server
if (!Global.State[name] || !initialState) {
Global.State = produce(Global.State, (s) => {
s[name] = s[model.__id]
})
}
if (initialState && initialState.__FROM_SERVER__) {
Global.State = produce(Global.State, (s) => {
s[name] = { ...s[model.__id], ...initialState[name] }
})
}
Global.Actions[name] = Global.Actions[model.__id]
Global.AsyncState[name] = Global.AsyncState[model.__id]
Global.Middlewares[name] = Global.Middlewares[model.__id]
Global.Context[name] = Global.Context[model.__id]
}
actions[n] = getActions(name)
})
return {
actions,
getActions: (name: string) => getActions(prefix + name),
getInitialState: async (
context?: T,
config?: { isServer?: boolean }
) => getInitialState(context, { ...config, prefix }),
getState: (name: string) => getState(prefix + name),
subscribe: (
name: string,
actions: keyof MT['actions'] | Array,
callback: () => void
) => subscribe(prefix + name, actions as string | string[], callback),
unsubscribe: (
name: string,
actionName: keyof MT['actions'] | Array
) => unsubscribe(prefix + name, actionName as string | string[]),
useStore: (name: string, selector?: Function) =>
useStore(prefix + name, selector)
} as APIs
}
}
const unsubscribe = (modelName: string, actions: string | string[]) => {
subscribe(modelName, actions, undefined)
}
const subscribe = (
modelName: string,
actions: string | string[],
callback?: () => void
) => {
if (Array.isArray(actions)) {
actions.forEach((actionName) => {
if (!Global.subscriptions[`${modelName}_${actionName}`]) {
Global.subscriptions[`${modelName}_${actionName}`] = []
}
if (callback) {
Global.subscriptions[`${modelName}_${actionName}`].push(callback)
} else {
Global.subscriptions[`${modelName}_${actionName}`] = []
}
})
} else {
if (!Global.subscriptions[`${modelName}_${actions}`]) {
Global.subscriptions[`${modelName}_${actions}`] = []
}
if (callback) {
Global.subscriptions[`${modelName}_${actions}`].push(callback)
} else {
Global.subscriptions[`${modelName}_${actions}`] = []
}
}
}
const getState = (modelName: keyof typeof Global.State) => {
return Global.State[modelName]
}
const getActions = (
modelName: string,
baseContext: Partial = { type: 'o' }
) => {
const updaters: any = {}
Object.keys(Global.Actions[modelName]).forEach(
(key) =>
(updaters[key] = async (params: any, middlewareConfig?: any) => {
const context: InnerContext = {
action: Global.Actions[modelName][key],
actionName: key,
consumerActions,
middlewareConfig,
modelName,
newState: null,
params,
...baseContext,
Global
}
if (Global.Middlewares[modelName]) {
return await applyMiddlewares(Global.Middlewares[modelName], context)
} else {
return await applyMiddlewares(actionMiddlewares, context)
}
})
)
return updaters
}
const useStore = (modelName: string, selector?: Function) => {
const setState = useState({})[1]
const hash = useRef('')
// createStore('xxx', () => {}) has the top priority
const mutableState = get([modelName])(Global.mutableState)
const isFromCreateStore = !!mutableState
const usedSelector = isFromCreateStore ? mutableState.selector : selector
const usedState = isFromCreateStore ? mutableState : getState(modelName)
useStoreEffect(() => {
Global.uid += 1
const local_hash = '' + Global.uid
hash.current = local_hash
if (!Global.Setter.functionSetter[modelName]) {
Global.Setter.functionSetter[modelName] = {}
}
Global.Setter.functionSetter[modelName][local_hash] = {
setState,
selector: usedSelector
}
return function cleanup() {
delete Global.Setter.functionSetter[modelName][local_hash]
}
}, [])
if (isFromCreateStore) {
return usedSelector(usedState)
} else {
const updaters = getActions(modelName, {
__hash: hash.current,
type: 'f'
})
return [usedSelector ? usedSelector(usedState) : usedState, updaters]
}
}
// Class API
class Provider extends PureComponent<{}, Global['State']> {
state = Global.State
render() {
const { children } = this.props
Global.Setter.classSetter = this.setState.bind(this)
return (
{children}
)
}
}
const connect =
(
modelName: string,
mapState?: Function | undefined,
mapActions?: Function | undefined
) =>
(Component: typeof React.Component | typeof PureComponent) =>
class P extends PureComponent {
render() {
const { state: prevState = {}, actions: prevActions = {} } = this.props
return (
{(models) => {
const { [`${modelName}`]: state } = models as any
const actions = Global.Actions[modelName]
return (
// @ts-ignore
)
}}
)
}
}
export {
actionMiddlewares,
createStore,
useModel,
Model,
middlewares,
Provider,
Consumer,
connect,
getState,
getInitialState
}