muya 1.1.0 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -139
- package/cjs/index.js +1 -1
- package/esm/__tests__/create-async.test.js +1 -0
- package/esm/__tests__/test-utils.js +1 -0
- package/esm/create.js +1 -1
- package/esm/debug/development-tools.js +1 -0
- package/esm/index.js +1 -1
- package/esm/subscriber.js +1 -0
- package/esm/types.js +1 -1
- package/esm/use.js +1 -0
- package/esm/utils/__tests__/context.test.js +1 -0
- package/esm/utils/__tests__/is.test.js +1 -0
- package/esm/utils/__tests__/shallow.test.js +1 -0
- package/esm/utils/__tests__/sub-memo.test.js +1 -0
- package/esm/utils/common.js +1 -0
- package/esm/utils/create-context.js +1 -0
- package/esm/utils/create-emitter.js +1 -0
- package/esm/utils/global-scheduler.js +1 -0
- package/esm/utils/is.js +1 -0
- package/esm/utils/scheduler.js +1 -0
- package/esm/utils/shallow.js +1 -0
- package/esm/utils/sub-memo.js +1 -0
- package/package.json +1 -1
- package/packages/core/__tests__/bench.test.tsx +266 -0
- package/packages/core/__tests__/create-async.test.ts +88 -0
- package/packages/core/__tests__/create.test.tsx +107 -0
- package/packages/core/__tests__/subscriber.test.tsx +89 -0
- package/packages/core/__tests__/test-utils.ts +40 -0
- package/packages/core/__tests__/use-async.test.tsx +45 -0
- package/packages/core/__tests__/use.test.tsx +125 -0
- package/packages/core/create.ts +98 -0
- package/packages/core/debug/development-tools.ts +37 -0
- package/packages/core/index.ts +4 -0
- package/packages/core/subscriber.ts +165 -0
- package/packages/core/types.ts +15 -0
- package/packages/core/use.ts +57 -0
- package/packages/core/utils/__tests__/context.test.ts +198 -0
- package/packages/core/utils/__tests__/is.test.ts +74 -0
- package/packages/core/utils/__tests__/shallow.test.ts +418 -0
- package/packages/core/utils/__tests__/sub-memo.test.ts +13 -0
- package/packages/core/utils/common.ts +48 -0
- package/packages/core/utils/create-context.ts +60 -0
- package/packages/core/utils/create-emitter.ts +55 -0
- package/packages/core/utils/global-scheduler.ts +75 -0
- package/packages/core/utils/is.ts +50 -0
- package/packages/core/utils/scheduler.ts +59 -0
- package/{src → packages/core/utils}/shallow.ts +3 -6
- package/packages/core/utils/sub-memo.ts +49 -0
- package/types/__tests__/test-utils.d.ts +20 -0
- package/types/create.d.ts +21 -21
- package/types/debug/development-tools.d.ts +10 -0
- package/types/index.d.ts +2 -4
- package/types/subscriber.d.ts +25 -0
- package/types/types.d.ts +9 -65
- package/types/use.d.ts +2 -0
- package/types/utils/common.d.ts +15 -0
- package/types/utils/create-context.d.ts +5 -0
- package/types/utils/create-emitter.d.ts +20 -0
- package/types/utils/global-scheduler.d.ts +5 -0
- package/types/utils/is.d.ts +13 -0
- package/types/utils/scheduler.d.ts +8 -0
- package/types/utils/sub-memo.d.ts +7 -0
- package/esm/common.js +0 -1
- package/esm/create-base-state.js +0 -1
- package/esm/create-emitter.js +0 -1
- package/esm/create-getter-state.js +0 -1
- package/esm/is.js +0 -1
- package/esm/merge.js +0 -1
- package/esm/select.js +0 -1
- package/esm/shallow.js +0 -1
- package/esm/use-state-value.js +0 -1
- package/src/common.ts +0 -28
- package/src/create-base-state.ts +0 -35
- package/src/create-emitter.ts +0 -24
- package/src/create-getter-state.ts +0 -19
- package/src/create.ts +0 -102
- package/src/index.ts +0 -6
- package/src/is.ts +0 -36
- package/src/merge.ts +0 -41
- package/src/select.ts +0 -33
- package/src/state.test.tsx +0 -647
- package/src/types.ts +0 -94
- package/src/use-state-value.ts +0 -29
- package/types/common.d.ts +0 -7
- package/types/create-base-state.d.ts +0 -10
- package/types/create-emitter.d.ts +0 -7
- package/types/create-getter-state.d.ts +0 -6
- package/types/is.d.ts +0 -10
- package/types/merge.d.ts +0 -2
- package/types/select.d.ts +0 -2
- package/types/use-state-value.d.ts +0 -10
- /package/types/{shallow.d.ts → utils/shallow.d.ts} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-hooks'
|
|
2
|
+
import { create } from '../create'
|
|
3
|
+
import { use } from '../use'
|
|
4
|
+
import { waitFor } from '@testing-library/react'
|
|
5
|
+
import { useCallback } from 'react'
|
|
6
|
+
import { getDebugCacheCreation } from '../utils/sub-memo'
|
|
7
|
+
|
|
8
|
+
describe('use-create', () => {
|
|
9
|
+
const reRendersBefore = jest.fn()
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should test use hook', async () => {
|
|
16
|
+
const state = create(1)
|
|
17
|
+
|
|
18
|
+
const { result } = renderHook(() => {
|
|
19
|
+
reRendersBefore()
|
|
20
|
+
return use(state)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
state.set(2)
|
|
24
|
+
|
|
25
|
+
await waitFor(() => {})
|
|
26
|
+
expect(result.current).toBe(2)
|
|
27
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
28
|
+
|
|
29
|
+
state.set(3)
|
|
30
|
+
|
|
31
|
+
await waitFor(() => {})
|
|
32
|
+
expect(result.current).toBe(3)
|
|
33
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(3)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should test derived state with multiple states', async () => {
|
|
37
|
+
const state1 = create(1)
|
|
38
|
+
const state2 = create(2)
|
|
39
|
+
|
|
40
|
+
function derivedBefore(plusValue: number) {
|
|
41
|
+
return state1() + state2() + plusValue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function derived() {
|
|
45
|
+
return state1() + state2() + derivedBefore(10)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { result } = renderHook(() => {
|
|
49
|
+
reRendersBefore()
|
|
50
|
+
return use(derived)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await waitFor(() => {})
|
|
54
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(1)
|
|
55
|
+
act(() => {
|
|
56
|
+
state1.set(2)
|
|
57
|
+
state2.set(3)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
await waitFor(() => {})
|
|
61
|
+
expect(result.current).toBe(20)
|
|
62
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should test use hook without memoize fn', async () => {
|
|
66
|
+
const state1 = create(1)
|
|
67
|
+
const state2 = create(2)
|
|
68
|
+
|
|
69
|
+
function derivedBefore(plusValue: number) {
|
|
70
|
+
return state1() + state2() + plusValue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function derived(add: number) {
|
|
74
|
+
return state1() + state2() + derivedBefore(add)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { result } = renderHook(() => {
|
|
78
|
+
reRendersBefore()
|
|
79
|
+
return use(() => derived(10))
|
|
80
|
+
})
|
|
81
|
+
expect(getDebugCacheCreation()).toBe(1)
|
|
82
|
+
|
|
83
|
+
await waitFor(() => {})
|
|
84
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(1)
|
|
85
|
+
act(() => {
|
|
86
|
+
state1.set(2)
|
|
87
|
+
state2.set(3)
|
|
88
|
+
})
|
|
89
|
+
expect(getDebugCacheCreation()).toBe(1)
|
|
90
|
+
await waitFor(() => {})
|
|
91
|
+
expect(result.current).toBe(20)
|
|
92
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should test use hook with memoize fn', async () => {
|
|
96
|
+
const state1 = create(1)
|
|
97
|
+
const state2 = create(2)
|
|
98
|
+
|
|
99
|
+
function derivedBefore(plusValue: number) {
|
|
100
|
+
return state1() + state2() + plusValue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function derived(add: number) {
|
|
104
|
+
return state1() + state2() + derivedBefore(add)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { result } = renderHook(() => {
|
|
108
|
+
reRendersBefore()
|
|
109
|
+
const memoized = useCallback(() => derived(10), [])
|
|
110
|
+
return use(memoized)
|
|
111
|
+
})
|
|
112
|
+
expect(getDebugCacheCreation()).toBe(1)
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {})
|
|
115
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(1)
|
|
116
|
+
act(() => {
|
|
117
|
+
state1.set(2)
|
|
118
|
+
state2.set(3)
|
|
119
|
+
})
|
|
120
|
+
expect(getDebugCacheCreation()).toBe(1)
|
|
121
|
+
await waitFor(() => {})
|
|
122
|
+
expect(result.current).toBe(20)
|
|
123
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { canUpdate, generateId } from './utils/common'
|
|
2
|
+
import type { Emitter } from './utils/create-emitter'
|
|
3
|
+
import { createEmitter } from './utils/create-emitter'
|
|
4
|
+
import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
|
|
5
|
+
// import { createScheduler } from './utils/scheduler'
|
|
6
|
+
import type { Cache, Callable, DefaultValue, IsEqual, Listener, SetValue } from './types'
|
|
7
|
+
import { context } from './subscriber'
|
|
8
|
+
import { createGlobalScheduler } from './utils/global-scheduler'
|
|
9
|
+
|
|
10
|
+
export const createScheduler = createGlobalScheduler()
|
|
11
|
+
interface RawState<T> {
|
|
12
|
+
(): T
|
|
13
|
+
id: number
|
|
14
|
+
set: (value: SetValue<T>) => void
|
|
15
|
+
emitter: Emitter<T>
|
|
16
|
+
listen: Listener<T>
|
|
17
|
+
destroy: () => void
|
|
18
|
+
withName: (name: string) => RawState<T>
|
|
19
|
+
stateName?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type State<T> = {
|
|
23
|
+
readonly [K in keyof RawState<T>]: RawState<T>[K]
|
|
24
|
+
} & Callable<T>
|
|
25
|
+
|
|
26
|
+
export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = isEqualBase): State<T> {
|
|
27
|
+
const cache: Cache<T> = {}
|
|
28
|
+
|
|
29
|
+
function getValue(): T {
|
|
30
|
+
if (isUndefined(cache.current)) {
|
|
31
|
+
cache.current = isFunction(initialValue) ? initialValue() : initialValue
|
|
32
|
+
}
|
|
33
|
+
return cache.current
|
|
34
|
+
}
|
|
35
|
+
function resolveValue(value: SetValue<T>) {
|
|
36
|
+
const previous = getValue()
|
|
37
|
+
cache.current = isSetValueFunction(value) ? value(previous) : value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// const schedule = createScheduler<SetValue<T>>({
|
|
41
|
+
// onFinish() {
|
|
42
|
+
// cache.current = getValue()
|
|
43
|
+
// if (!canUpdate(cache, isEqual)) {
|
|
44
|
+
// return
|
|
45
|
+
// }
|
|
46
|
+
// state.emitter.emit()
|
|
47
|
+
// },
|
|
48
|
+
// onResolveItem: resolveValue,
|
|
49
|
+
// })
|
|
50
|
+
|
|
51
|
+
const state: RawState<T> = function () {
|
|
52
|
+
const stateValue = getValue()
|
|
53
|
+
const ctx = context.use()
|
|
54
|
+
// console.log('CTX', ctx?.id, 'STATE', state.id)
|
|
55
|
+
if (ctx && !state.emitter.contains(ctx.sub)) {
|
|
56
|
+
ctx.addEmitter(state.emitter)
|
|
57
|
+
}
|
|
58
|
+
return stateValue
|
|
59
|
+
}
|
|
60
|
+
state.listen = function (listener: (value: T) => void) {
|
|
61
|
+
return state.emitter.subscribe(() => {
|
|
62
|
+
const final = cache.current
|
|
63
|
+
if (isUndefined(final)) {
|
|
64
|
+
throw new Error('The value is undefined')
|
|
65
|
+
}
|
|
66
|
+
listener(final)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
state.emitter = createEmitter<T>(() => state())
|
|
70
|
+
state.id = generateId()
|
|
71
|
+
|
|
72
|
+
const clearScheduler = createScheduler.add(state.id, {
|
|
73
|
+
onFinish() {
|
|
74
|
+
cache.current = getValue()
|
|
75
|
+
if (!canUpdate(cache, isEqual)) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
state.emitter.emit()
|
|
79
|
+
},
|
|
80
|
+
onResolveItem: resolveValue,
|
|
81
|
+
})
|
|
82
|
+
state.set = function (value) {
|
|
83
|
+
createScheduler.schedule(state.id, value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
state.destroy = function () {
|
|
87
|
+
cache.current = undefined
|
|
88
|
+
getValue()
|
|
89
|
+
clearScheduler()
|
|
90
|
+
state.emitter.clear()
|
|
91
|
+
}
|
|
92
|
+
state.withName = function (name: string) {
|
|
93
|
+
state.stateName = name
|
|
94
|
+
return state
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return state
|
|
98
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isPromise } from '../utils/is'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4
|
+
// @ts-expect-error
|
|
5
|
+
const reduxDevelopmentTools = window?.__REDUX_DEVTOOLS_EXTENSION__?.connect({
|
|
6
|
+
name: 'CustomState', // This will name your instance in the DevTools
|
|
7
|
+
trace: true, // Enables trace if needed
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
if (reduxDevelopmentTools) {
|
|
11
|
+
reduxDevelopmentTools.init({ message: 'Initial state' })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type StateType = 'state' | 'derived'
|
|
15
|
+
|
|
16
|
+
interface SendOptions {
|
|
17
|
+
message?: string
|
|
18
|
+
type: StateType
|
|
19
|
+
value: unknown
|
|
20
|
+
name: string
|
|
21
|
+
}
|
|
22
|
+
export function sendToDevelopmentTools(options: SendOptions) {
|
|
23
|
+
if (!reduxDevelopmentTools) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
const { message, type, value, name } = options
|
|
27
|
+
if (isPromise(value)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
reduxDevelopmentTools.send(name, { value, type, message }, type)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function developmentToolsListener(name: string, type: StateType) {
|
|
34
|
+
return (value: unknown) => {
|
|
35
|
+
sendToDevelopmentTools({ name, type, value, message: 'update' })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { AnyFunction, Cache, Callable, Listener } from './types'
|
|
2
|
+
import type { CancelablePromise } from './utils/common'
|
|
3
|
+
import { cancelablePromise, canUpdate, generateId } from './utils/common'
|
|
4
|
+
import { createContext } from './utils/create-context'
|
|
5
|
+
import type { Emitter } from './utils/create-emitter'
|
|
6
|
+
import { createEmitter } from './utils/create-emitter'
|
|
7
|
+
import type { StateType } from './debug/development-tools'
|
|
8
|
+
import { developmentToolsListener, sendToDevelopmentTools } from './debug/development-tools'
|
|
9
|
+
import { createGlobalScheduler } from './utils/global-scheduler'
|
|
10
|
+
import { isAbortError, isCreate, isEqualBase, isPromise, isUndefined } from './utils/is'
|
|
11
|
+
|
|
12
|
+
const subscriberScheduler = createGlobalScheduler()
|
|
13
|
+
interface SubscribeContext<T = unknown> {
|
|
14
|
+
addEmitter: (emitter: Emitter<T>) => void
|
|
15
|
+
id: number
|
|
16
|
+
sub: () => void
|
|
17
|
+
}
|
|
18
|
+
interface SubscribeRaw<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>> {
|
|
19
|
+
(): T
|
|
20
|
+
emitter: Emitter<T | undefined>
|
|
21
|
+
destroy: () => void
|
|
22
|
+
id: number
|
|
23
|
+
listen: Listener<T>
|
|
24
|
+
abort: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Subscribe<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>> = {
|
|
28
|
+
readonly [K in keyof SubscribeRaw<F, T>]: SubscribeRaw<F, T>[K]
|
|
29
|
+
} & Callable<T>
|
|
30
|
+
|
|
31
|
+
export const context = createContext<SubscribeContext | undefined>(undefined)
|
|
32
|
+
|
|
33
|
+
export function subscriber<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>>(
|
|
34
|
+
anyFunction: () => T,
|
|
35
|
+
): Subscribe<F, T> {
|
|
36
|
+
const cache: Cache<T> = {}
|
|
37
|
+
let isInitialized = false
|
|
38
|
+
const cleaners: Array<() => void> = []
|
|
39
|
+
const promiseData: CancelablePromise<T> = {}
|
|
40
|
+
|
|
41
|
+
const emitter = createEmitter(
|
|
42
|
+
() => {
|
|
43
|
+
if (!isInitialized) {
|
|
44
|
+
isInitialized = true
|
|
45
|
+
return subscribe()
|
|
46
|
+
}
|
|
47
|
+
return cache.current
|
|
48
|
+
},
|
|
49
|
+
() => {
|
|
50
|
+
isInitialized = true
|
|
51
|
+
return subscribe()
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async function sub() {
|
|
56
|
+
if (!canUpdate(cache, isEqualBase)) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (promiseData.controller) {
|
|
60
|
+
promiseData.controller.abort()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
subscriberScheduler.schedule(id, null)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const id = generateId()
|
|
67
|
+
|
|
68
|
+
const clearScheduler = subscriberScheduler.add(id, {
|
|
69
|
+
onFinish() {
|
|
70
|
+
cache.current = subscribe()
|
|
71
|
+
emitter.emit()
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
const ctx: SubscribeContext = {
|
|
75
|
+
addEmitter(stateEmitter) {
|
|
76
|
+
const clean = stateEmitter.subscribe(sub)
|
|
77
|
+
cleaners.push(clean)
|
|
78
|
+
},
|
|
79
|
+
id,
|
|
80
|
+
sub,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function asyncSub(resultValue: Promise<T>): Promise<T> | undefined {
|
|
84
|
+
const cancel = cancelablePromise<T>(resultValue, promiseData.controller)
|
|
85
|
+
promiseData.controller = cancel.controller
|
|
86
|
+
cancel.promise
|
|
87
|
+
?.then((value) => {
|
|
88
|
+
cache.current = value
|
|
89
|
+
emitter.emit()
|
|
90
|
+
})
|
|
91
|
+
.catch((error) => {
|
|
92
|
+
if (isAbortError(error)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
cache.current = error
|
|
96
|
+
emitter.emit()
|
|
97
|
+
})
|
|
98
|
+
return cancel.promise
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const subscribe = function (): T {
|
|
102
|
+
const resultValue = context.run(ctx, anyFunction)
|
|
103
|
+
|
|
104
|
+
if (!isPromise(resultValue)) {
|
|
105
|
+
cache.current = resultValue
|
|
106
|
+
return resultValue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const promise = context.wrap(() => asyncSub(resultValue))()
|
|
110
|
+
if (isPromise(promise)) {
|
|
111
|
+
// we do not do anything with the promise, because it is already handled in asyncSub
|
|
112
|
+
promise.catch(() => null)
|
|
113
|
+
}
|
|
114
|
+
const promiseAsT = promise as T
|
|
115
|
+
cache.current = promiseAsT
|
|
116
|
+
return promiseAsT
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
subscribe.emitter = emitter
|
|
120
|
+
subscribe.destroy = function () {
|
|
121
|
+
for (const cleaner of cleaners) {
|
|
122
|
+
cleaner()
|
|
123
|
+
}
|
|
124
|
+
emitter.clear()
|
|
125
|
+
clearScheduler()
|
|
126
|
+
}
|
|
127
|
+
subscribe.id = id
|
|
128
|
+
subscribe.listen = function (listener: (value: T) => void) {
|
|
129
|
+
return emitter.subscribe(() => {
|
|
130
|
+
const final = cache.current
|
|
131
|
+
if (isUndefined(final)) {
|
|
132
|
+
throw new Error('The value is undefined')
|
|
133
|
+
}
|
|
134
|
+
listener(final)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (process.env.NODE_ENV === 'development') {
|
|
139
|
+
let name: string | undefined
|
|
140
|
+
let type: StateType = 'derived'
|
|
141
|
+
if (isCreate(anyFunction)) {
|
|
142
|
+
type = 'state'
|
|
143
|
+
name = anyFunction.stateName
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!name) {
|
|
147
|
+
name = anyFunction.name.length > 0 ? anyFunction.name : anyFunction.toString()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
sendToDevelopmentTools({
|
|
151
|
+
name,
|
|
152
|
+
type,
|
|
153
|
+
value: subscribe(),
|
|
154
|
+
message: 'init',
|
|
155
|
+
})
|
|
156
|
+
const listener = developmentToolsListener(name, type)
|
|
157
|
+
subscribe.listen(listener)
|
|
158
|
+
}
|
|
159
|
+
subscribe.abort = function () {
|
|
160
|
+
if (promiseData.controller) {
|
|
161
|
+
promiseData.controller.abort()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return subscribe
|
|
165
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export type AnyFunction = (...args: any[]) => any
|
|
3
|
+
|
|
4
|
+
export type IsEqual<T = unknown> = (a: T, b: T) => boolean
|
|
5
|
+
export type SetStateCb<T> = (value: T) => Awaited<T>
|
|
6
|
+
export type SetValue<T> = SetStateCb<T> | Awaited<T>
|
|
7
|
+
export type DefaultValue<T> = T | (() => T)
|
|
8
|
+
export type Listener<T> = (listener: (value: T) => void) => () => void
|
|
9
|
+
export interface Cache<T> {
|
|
10
|
+
current?: T
|
|
11
|
+
previous?: T
|
|
12
|
+
}
|
|
13
|
+
export type Callable<T> = () => T
|
|
14
|
+
|
|
15
|
+
export const EMPTY_SELECTOR = <T>(stateValue: T) => stateValue
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
/* eslint-disable sonarjs/rules-of-hooks */
|
|
3
|
+
import { useDebugValue, useEffect, useRef } from 'react'
|
|
4
|
+
import { EMPTY_SELECTOR, type AnyFunction } from './types'
|
|
5
|
+
import { isAnyOtherError, isPromise } from './utils/is'
|
|
6
|
+
import { useSyncExternalStore } from 'react'
|
|
7
|
+
import { subMemo } from './utils/sub-memo'
|
|
8
|
+
|
|
9
|
+
const PROMOTE_DEBUG_AFTER_REACH_TIMES = 10
|
|
10
|
+
const PROMOTE_DEBUG_AFTER_REACH_COUNT = 3
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
12
|
+
function useDebugFunction<F extends AnyFunction>(function_: F) {
|
|
13
|
+
const renderCount = useRef({ renders: 0, startTime: performance.now() })
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
renderCount.current.renders++
|
|
16
|
+
const passedTime = performance.now() - renderCount.current.startTime
|
|
17
|
+
if (passedTime < PROMOTE_DEBUG_AFTER_REACH_TIMES) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if (renderCount.current.renders < PROMOTE_DEBUG_AFTER_REACH_COUNT) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
renderCount.current.startTime = performance.now()
|
|
24
|
+
renderCount.current.renders = 0
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.warn(
|
|
27
|
+
`Function ${function_.name.length > 0 ? function_.name : function_} seems to be not memoized, wrap the function to the useCallback or use global defined functions.`,
|
|
28
|
+
)
|
|
29
|
+
}, [function_])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function use<F extends AnyFunction, T extends ReturnType<F>, S extends ReturnType<F>>(
|
|
33
|
+
anyFunction: () => T,
|
|
34
|
+
selector: (stateValue: T) => S = EMPTY_SELECTOR,
|
|
35
|
+
): undefined extends S ? T : S {
|
|
36
|
+
const memo = subMemo(anyFunction)
|
|
37
|
+
const sub = memo.call()
|
|
38
|
+
const initialSnapshot = sub.emitter.getInitialSnapshot ?? sub.emitter.getSnapshot
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
return memo.destroy
|
|
41
|
+
}, [anyFunction, memo.destroy])
|
|
42
|
+
|
|
43
|
+
const value = useSyncExternalStore<S>(
|
|
44
|
+
sub.emitter.subscribe,
|
|
45
|
+
() => selector(sub.emitter.getSnapshot() as T),
|
|
46
|
+
() => selector(initialSnapshot() as T),
|
|
47
|
+
)
|
|
48
|
+
useDebugValue(value)
|
|
49
|
+
if (isPromise(value)) {
|
|
50
|
+
throw value
|
|
51
|
+
}
|
|
52
|
+
if (isAnyOtherError(value)) {
|
|
53
|
+
memo.destroy()
|
|
54
|
+
throw value
|
|
55
|
+
}
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* eslint-disable sonarjs/pseudo-random */
|
|
2
|
+
/* eslint-disable sonarjs/no-nested-functions */
|
|
3
|
+
import { createContext } from '../create-context'
|
|
4
|
+
import { longPromise } from '../../__tests__/test-utils'
|
|
5
|
+
|
|
6
|
+
describe('context', () => {
|
|
7
|
+
it('should check context', () => {
|
|
8
|
+
const context = createContext({ name: 'John Doe' })
|
|
9
|
+
|
|
10
|
+
const main = () => {
|
|
11
|
+
context.run({ name: 'Jane Doe' }, () => {
|
|
12
|
+
expect(context.use()).toEqual({ name: 'Jane Doe' })
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
expect(context.use()).toEqual({ name: 'John Doe' })
|
|
16
|
+
main()
|
|
17
|
+
expect(context.use()).toEqual({ name: 'John Doe' })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should test async context', (done) => {
|
|
21
|
+
const context = createContext<string>('empty')
|
|
22
|
+
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
23
|
+
const awaiter = async () => new Promise((resolve) => setTimeout(resolve, 10))
|
|
24
|
+
context.run('outer', () => {
|
|
25
|
+
expect(context.use()).toEqual('outer')
|
|
26
|
+
|
|
27
|
+
// Wrap the asynchronous callback to preserve 'outer' context
|
|
28
|
+
setTimeout(
|
|
29
|
+
context.wrap(async () => {
|
|
30
|
+
try {
|
|
31
|
+
await awaiter()
|
|
32
|
+
expect(context.use()).toEqual('outer')
|
|
33
|
+
innerDone()
|
|
34
|
+
} catch (error) {
|
|
35
|
+
done(error)
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
10,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
context.run('inner', () => {
|
|
42
|
+
expect(context.use()).toEqual('inner')
|
|
43
|
+
|
|
44
|
+
// Wrap the asynchronous callback to preserve 'inner' context
|
|
45
|
+
setTimeout(
|
|
46
|
+
context.wrap(() => {
|
|
47
|
+
try {
|
|
48
|
+
expect(context.use()).toEqual('inner')
|
|
49
|
+
innerDone()
|
|
50
|
+
} catch (error) {
|
|
51
|
+
done(error)
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
10,
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(context.use()).toEqual('outer')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
let completed = 0
|
|
62
|
+
function innerDone() {
|
|
63
|
+
completed += 1
|
|
64
|
+
if (completed === 2) {
|
|
65
|
+
done()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
it('should test async nested context', (done) => {
|
|
70
|
+
const context = createContext(0)
|
|
71
|
+
context.run(1, () => {
|
|
72
|
+
expect(context.use()).toEqual(1)
|
|
73
|
+
context.run(2, () => {
|
|
74
|
+
context.run(3, () => {
|
|
75
|
+
expect(context.use()).toEqual(3)
|
|
76
|
+
setTimeout(
|
|
77
|
+
context.wrap(() => {
|
|
78
|
+
expect(context.use()).toEqual(3)
|
|
79
|
+
}),
|
|
80
|
+
10,
|
|
81
|
+
)
|
|
82
|
+
expect(context.use()).toEqual(3)
|
|
83
|
+
})
|
|
84
|
+
setTimeout(
|
|
85
|
+
context.wrap(() => {
|
|
86
|
+
expect(context.use()).toEqual(2)
|
|
87
|
+
}),
|
|
88
|
+
10,
|
|
89
|
+
)
|
|
90
|
+
expect(context.use()).toEqual(2)
|
|
91
|
+
context.run(3, () => {
|
|
92
|
+
expect(context.use()).toEqual(3)
|
|
93
|
+
setTimeout(
|
|
94
|
+
context.wrap(() => {
|
|
95
|
+
expect(context.use()).toEqual(3)
|
|
96
|
+
context.run(4, () => {
|
|
97
|
+
expect(context.use()).toEqual(4)
|
|
98
|
+
setTimeout(
|
|
99
|
+
context.wrap(() => {
|
|
100
|
+
expect(context.use()).toEqual(4)
|
|
101
|
+
done()
|
|
102
|
+
}),
|
|
103
|
+
10,
|
|
104
|
+
)
|
|
105
|
+
expect(context.use()).toEqual(4)
|
|
106
|
+
})
|
|
107
|
+
}),
|
|
108
|
+
10,
|
|
109
|
+
)
|
|
110
|
+
expect(context.use()).toEqual(3)
|
|
111
|
+
})
|
|
112
|
+
// check back to 2
|
|
113
|
+
expect(context.use()).toEqual(2)
|
|
114
|
+
})
|
|
115
|
+
// check back to 1
|
|
116
|
+
expect(context.use()).toEqual(1)
|
|
117
|
+
})
|
|
118
|
+
// check back to 0
|
|
119
|
+
expect(context.use()).toEqual(0)
|
|
120
|
+
})
|
|
121
|
+
it('should stress test context with async random code', async () => {
|
|
122
|
+
const stressCount = 10_000
|
|
123
|
+
const context = createContext(0)
|
|
124
|
+
for (let index = 0; index < stressCount; index++) {
|
|
125
|
+
context.run(index, () => {
|
|
126
|
+
expect(context.use()).toEqual(index)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const promises: Promise<unknown>[] = []
|
|
131
|
+
for (let index = 0; index < stressCount; index++) {
|
|
132
|
+
context.run(index, () => {
|
|
133
|
+
expect(context.use()).toEqual(index)
|
|
134
|
+
const promise = new Promise((resolve) => {
|
|
135
|
+
setTimeout(
|
|
136
|
+
context.wrap(() => {
|
|
137
|
+
expect(context.use()).toEqual(index)
|
|
138
|
+
resolve(index)
|
|
139
|
+
}),
|
|
140
|
+
Math.random() * 100,
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
promises.push(promise)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
await Promise.all(promises)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should-test-default-value-with-ctx', async () => {
|
|
150
|
+
const ctx = createContext({ counter: 1 })
|
|
151
|
+
ctx.run({ counter: 10 }, async () => {
|
|
152
|
+
await longPromise(10)
|
|
153
|
+
expect(ctx.use()?.counter).toBe(10)
|
|
154
|
+
})
|
|
155
|
+
ctx.run({ counter: 12 }, () => {
|
|
156
|
+
expect(ctx.use()?.counter).toBe(12)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
it('should test nested context', () => {
|
|
160
|
+
const currentCount = 0
|
|
161
|
+
const context = createContext({ count: 0 })
|
|
162
|
+
function container() {
|
|
163
|
+
const isIn = context.use()
|
|
164
|
+
expect(isIn?.count).toBe(currentCount)
|
|
165
|
+
context.run({ count: currentCount + 1 }, () => {
|
|
166
|
+
const inner = context.use()
|
|
167
|
+
expect(inner?.count).toBe(currentCount + 1)
|
|
168
|
+
context.run({ count: currentCount + 2 }, () => {
|
|
169
|
+
const innerInner = context.use()
|
|
170
|
+
expect(innerInner?.count).toBe(currentCount + 2)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
container()
|
|
176
|
+
container()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should test nested context with async when promise is returned, but not waited', async () => {
|
|
180
|
+
const context = createContext({ count: 0 })
|
|
181
|
+
|
|
182
|
+
async function insideContextNestedAwaited() {
|
|
183
|
+
await longPromise(10)
|
|
184
|
+
const ctx = context.use()
|
|
185
|
+
expect(ctx?.count).toBe(1)
|
|
186
|
+
}
|
|
187
|
+
async function insideContext() {
|
|
188
|
+
await insideContextNestedAwaited()
|
|
189
|
+
// HERE THIS IS NOT WAITED, SO CONTEXT IS LOST.
|
|
190
|
+
// insideContextNestedAwaited() // this will not work
|
|
191
|
+
context.wrap(insideContextNestedAwaited) // this work
|
|
192
|
+
const ctx = context.use()
|
|
193
|
+
expect(ctx?.count).toBe(1)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
context.run({ count: 1 }, insideContext)
|
|
197
|
+
})
|
|
198
|
+
})
|