muya 1.0.3 → 2.0.0-beta.1
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 +194 -216
- package/cjs/index.js +1 -1
- package/esm/__tests__/create-async.test.js +1 -0
- package/esm/create.js +1 -1
- 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__/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/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 -4
- package/packages/core/__tests__/bench.test.tsx +261 -0
- package/packages/core/__tests__/create-async.test.ts +88 -0
- package/packages/core/__tests__/create.test.tsx +107 -0
- package/packages/core/__tests__/use-async.test.tsx +44 -0
- package/packages/core/__tests__/use.test.tsx +76 -0
- package/packages/core/create.ts +67 -0
- package/packages/core/index.ts +4 -0
- package/packages/core/subscriber.ts +121 -0
- package/packages/core/types.ts +15 -0
- package/packages/core/use.ts +59 -0
- package/packages/core/utils/__tests__/context.test.ts +198 -0
- package/{src → packages/core/utils}/__tests__/is.test.ts +1 -30
- 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 +53 -0
- package/{src → packages/core/utils}/is.ts +11 -13
- package/packages/core/utils/scheduler.ts +59 -0
- package/{src → packages/core/utils}/shallow.ts +3 -3
- package/packages/core/utils/sub-memo.ts +37 -0
- package/types/create.d.ts +14 -21
- 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/{is.d.ts → utils/is.d.ts} +5 -6
- package/types/utils/scheduler.d.ts +6 -0
- package/types/utils/sub-memo.d.ts +6 -0
- package/esm/__tests__/common.test.js +0 -1
- package/esm/__tests__/is.test.js +0 -1
- package/esm/__tests__/merge.test.js +0 -1
- package/esm/__tests__/types.test.js +0 -1
- 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/__tests__/common.test.ts +0 -63
- package/src/__tests__/create.test.tsx +0 -84
- package/src/__tests__/merge.test.ts +0 -78
- package/src/__tests__/state.test.tsx +0 -619
- package/src/__tests__/types.test.ts +0 -17
- package/src/common.ts +0 -60
- package/src/create-base-state.ts +0 -31
- package/src/create-emitter.ts +0 -24
- package/src/create-getter-state.ts +0 -18
- package/src/create.ts +0 -127
- package/src/index.ts +0 -6
- package/src/merge.ts +0 -38
- package/src/select.ts +0 -33
- package/src/types.ts +0 -94
- package/src/use-state-value.ts +0 -32
- package/types/common.d.ts +0 -17
- 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/merge.d.ts +0 -4
- package/types/select.d.ts +0 -2
- package/types/use-state-value.d.ts +0 -10
- /package/esm/{__tests__ → utils/__tests__}/shallow.test.js +0 -0
- /package/{src → packages/core}/__tests__/test-utils.ts +0 -0
- /package/{src → packages/core/utils}/__tests__/shallow.test.ts +0 -0
- /package/types/{shallow.d.ts → utils/shallow.d.ts} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
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 { isAbortError, isEqualBase, isPromise, isUndefined } from './utils/is'
|
|
8
|
+
|
|
9
|
+
interface SubscribeContext<T = unknown> {
|
|
10
|
+
addEmitter: (emitter: Emitter<T>) => void
|
|
11
|
+
id: number
|
|
12
|
+
sub: () => void
|
|
13
|
+
}
|
|
14
|
+
interface SubscribeRaw<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>> {
|
|
15
|
+
(): T
|
|
16
|
+
emitter: Emitter<T | undefined>
|
|
17
|
+
destroy: () => void
|
|
18
|
+
id: number
|
|
19
|
+
listen: Listener<T>
|
|
20
|
+
abort: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Subscribe<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>> = {
|
|
24
|
+
readonly [K in keyof SubscribeRaw<F, T>]: SubscribeRaw<F, T>[K]
|
|
25
|
+
} & Callable<T>
|
|
26
|
+
|
|
27
|
+
export const context = createContext<SubscribeContext | undefined>(undefined)
|
|
28
|
+
|
|
29
|
+
export function subscriber<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>>(
|
|
30
|
+
anyFunction: () => T,
|
|
31
|
+
): Subscribe<F, T> {
|
|
32
|
+
const cleaners: Array<() => void> = []
|
|
33
|
+
const promiseData: CancelablePromise<T> = {}
|
|
34
|
+
|
|
35
|
+
const cache: Cache<T> = {}
|
|
36
|
+
let isInitialized = false
|
|
37
|
+
const emitter = createEmitter(
|
|
38
|
+
() => {
|
|
39
|
+
if (!isInitialized) {
|
|
40
|
+
isInitialized = true
|
|
41
|
+
return subscribe()
|
|
42
|
+
}
|
|
43
|
+
return cache.current
|
|
44
|
+
},
|
|
45
|
+
() => {
|
|
46
|
+
isInitialized = true
|
|
47
|
+
return subscribe()
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async function sub() {
|
|
52
|
+
if (!canUpdate(cache, isEqualBase)) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (promiseData.controller) {
|
|
56
|
+
promiseData.controller.abort()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cache.current = subscribe()
|
|
60
|
+
emitter.emit()
|
|
61
|
+
}
|
|
62
|
+
const id = generateId()
|
|
63
|
+
const ctx: SubscribeContext = {
|
|
64
|
+
addEmitter(stateEmitter) {
|
|
65
|
+
const clean = stateEmitter.subscribe(sub)
|
|
66
|
+
cleaners.push(clean)
|
|
67
|
+
},
|
|
68
|
+
id,
|
|
69
|
+
sub,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const subscribe = function (): T {
|
|
73
|
+
const resultValue = context.run(ctx, anyFunction)
|
|
74
|
+
|
|
75
|
+
if (isPromise(resultValue)) {
|
|
76
|
+
const { controller, promise: promiseWithSelector } = cancelablePromise<T>(resultValue, promiseData.controller)
|
|
77
|
+
promiseData.controller = controller
|
|
78
|
+
promiseWithSelector
|
|
79
|
+
?.then((value) => {
|
|
80
|
+
cache.current = value
|
|
81
|
+
emitter.emit()
|
|
82
|
+
})
|
|
83
|
+
.catch((error) => {
|
|
84
|
+
if (isAbortError(error)) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw error
|
|
89
|
+
})
|
|
90
|
+
const promiseResult = promiseWithSelector as T
|
|
91
|
+
cache.current = promiseResult
|
|
92
|
+
return promiseResult
|
|
93
|
+
}
|
|
94
|
+
cache.current = resultValue
|
|
95
|
+
return resultValue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
subscribe.emitter = emitter
|
|
99
|
+
subscribe.destroy = function () {
|
|
100
|
+
for (const cleaner of cleaners) {
|
|
101
|
+
cleaner()
|
|
102
|
+
}
|
|
103
|
+
emitter.clear()
|
|
104
|
+
}
|
|
105
|
+
subscribe.id = id
|
|
106
|
+
subscribe.listen = function (listener: (value: T) => void) {
|
|
107
|
+
return emitter.subscribe(() => {
|
|
108
|
+
const final = cache.current
|
|
109
|
+
if (isUndefined(final)) {
|
|
110
|
+
throw new Error('The value is undefined')
|
|
111
|
+
}
|
|
112
|
+
listener(final)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
subscribe.abort = function () {
|
|
116
|
+
if (promiseData.controller) {
|
|
117
|
+
promiseData.controller.abort()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return subscribe
|
|
121
|
+
}
|
|
@@ -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,59 @@
|
|
|
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
|
+
// memo.call()
|
|
41
|
+
return memo.destroy
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [anyFunction])
|
|
44
|
+
|
|
45
|
+
const value = useSyncExternalStore<S>(
|
|
46
|
+
sub.emitter.subscribe,
|
|
47
|
+
() => selector(sub.emitter.getSnapshot() as T),
|
|
48
|
+
() => selector(initialSnapshot() as T),
|
|
49
|
+
)
|
|
50
|
+
useDebugValue(value)
|
|
51
|
+
if (isPromise(value)) {
|
|
52
|
+
throw value
|
|
53
|
+
}
|
|
54
|
+
if (isAnyOtherError(value)) {
|
|
55
|
+
memo.destroy()
|
|
56
|
+
throw value
|
|
57
|
+
}
|
|
58
|
+
return value
|
|
59
|
+
}
|
|
@@ -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
|
+
})
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
import { Abort } from '../common'
|
|
2
|
-
import {
|
|
3
|
-
isPromise,
|
|
4
|
-
isFunction,
|
|
5
|
-
isSetValueFunction,
|
|
6
|
-
isObject,
|
|
7
|
-
isRef,
|
|
8
|
-
isMap,
|
|
9
|
-
isSet,
|
|
10
|
-
isArray,
|
|
11
|
-
isEqualBase,
|
|
12
|
-
isAbortError,
|
|
13
|
-
} from '../is'
|
|
2
|
+
import { isPromise, isFunction, isSetValueFunction, isMap, isSet, isArray, isEqualBase, isAbortError } from '../is'
|
|
14
3
|
|
|
15
4
|
describe('isPromise', () => {
|
|
16
5
|
it('should return true for a Promise', () => {
|
|
@@ -39,24 +28,6 @@ describe('isSetValueFunction', () => {
|
|
|
39
28
|
})
|
|
40
29
|
})
|
|
41
30
|
|
|
42
|
-
describe('isObject', () => {
|
|
43
|
-
it('should return true for an object', () => {
|
|
44
|
-
expect(isObject({})).toBe(true)
|
|
45
|
-
})
|
|
46
|
-
it('should return false for a non-object', () => {
|
|
47
|
-
expect(isObject(123)).toBe(false)
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('isRef', () => {
|
|
52
|
-
it('should return true for a ref object', () => {
|
|
53
|
-
expect(isRef({ isRef: true })).toBe(true)
|
|
54
|
-
})
|
|
55
|
-
it('should return false for a non-ref object', () => {
|
|
56
|
-
expect(isRef({})).toBe(false)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
31
|
describe('isMap', () => {
|
|
61
32
|
it('should return true for a Map', () => {
|
|
62
33
|
expect(isMap(new Map())).toBe(true)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* eslint-disable unicorn/consistent-function-scoping */
|
|
2
|
+
import { subMemo } from '../sub-memo'
|
|
3
|
+
|
|
4
|
+
describe('memo-fn', () => {
|
|
5
|
+
it('should create memo fn', () => {
|
|
6
|
+
function toBeMemoized(): boolean {
|
|
7
|
+
return true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const memoized = subMemo(toBeMemoized)
|
|
11
|
+
expect(memoized.call().emitter).toBeDefined()
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Cache, IsEqual } from '../types'
|
|
2
|
+
import { isUndefined } from './is'
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line no-shadow
|
|
5
|
+
export enum Abort {
|
|
6
|
+
Error = 'StateAbortError',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CancelablePromise<T> {
|
|
10
|
+
promise?: Promise<T>
|
|
11
|
+
controller?: AbortController
|
|
12
|
+
resolveInitialPromise?: (value: T) => void
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Cancelable promise function, return promise and controller
|
|
16
|
+
*/
|
|
17
|
+
export function cancelablePromise<T>(promise: Promise<T>, previousController?: AbortController): CancelablePromise<T> {
|
|
18
|
+
if (previousController) {
|
|
19
|
+
previousController.abort()
|
|
20
|
+
}
|
|
21
|
+
const controller = new AbortController()
|
|
22
|
+
const { signal } = controller
|
|
23
|
+
|
|
24
|
+
const cancelable = new Promise<T>((resolve, reject) => {
|
|
25
|
+
// Listen for the abort event
|
|
26
|
+
signal.addEventListener('abort', () => {
|
|
27
|
+
reject(new DOMException('Promise was aborted', Abort.Error))
|
|
28
|
+
})
|
|
29
|
+
// When the original promise settles, resolve or reject accordingly
|
|
30
|
+
promise.then(resolve).catch(reject)
|
|
31
|
+
})
|
|
32
|
+
return { promise: cancelable, controller }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let id = 0
|
|
36
|
+
export function generateId() {
|
|
37
|
+
return id++
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function canUpdate<T>(cache: Cache<T>, isEqual: IsEqual<T> = (previous, next) => previous === next): boolean {
|
|
41
|
+
if (!isUndefined(cache.current)) {
|
|
42
|
+
if (!isUndefined(cache.previous) && isEqual(cache.current, cache.previous)) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
cache.previous = cache.current
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isPromise } from './is'
|
|
2
|
+
|
|
3
|
+
const EMPTY_CONTEXT = Symbol('_')
|
|
4
|
+
|
|
5
|
+
export function createContext<T>(defaultContextValue: T) {
|
|
6
|
+
const contextStack: Array<T | typeof EMPTY_CONTEXT> = []
|
|
7
|
+
|
|
8
|
+
function use(): T | undefined {
|
|
9
|
+
if (contextStack.length === 0) {
|
|
10
|
+
return defaultContextValue
|
|
11
|
+
}
|
|
12
|
+
const currentContext = contextStack.at(-1)
|
|
13
|
+
return currentContext === EMPTY_CONTEXT ? defaultContextValue : currentContext
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run<R>(ctxValue: T, cb: () => R | Promise<R>): R {
|
|
17
|
+
contextStack.push(ctxValue)
|
|
18
|
+
const result = cb()
|
|
19
|
+
const isResultPromise = isPromise(result)
|
|
20
|
+
if (isResultPromise) {
|
|
21
|
+
return (async () => {
|
|
22
|
+
try {
|
|
23
|
+
return await result
|
|
24
|
+
} finally {
|
|
25
|
+
contextStack.pop()
|
|
26
|
+
}
|
|
27
|
+
})() as R
|
|
28
|
+
} else {
|
|
29
|
+
contextStack.pop()
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function wrap<X>(cb: () => X | Promise<X>): () => X | Promise<X> {
|
|
35
|
+
const capturedContext = use()
|
|
36
|
+
return () => {
|
|
37
|
+
contextStack.push(capturedContext!)
|
|
38
|
+
const result = cb()
|
|
39
|
+
const isResultPromise = isPromise(result)
|
|
40
|
+
if (isResultPromise) {
|
|
41
|
+
return (async () => {
|
|
42
|
+
try {
|
|
43
|
+
return await result
|
|
44
|
+
} finally {
|
|
45
|
+
contextStack.pop()
|
|
46
|
+
}
|
|
47
|
+
})()
|
|
48
|
+
} else {
|
|
49
|
+
contextStack.pop()
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
run,
|
|
57
|
+
use,
|
|
58
|
+
wrap,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type EmitterSubscribe<P = undefined> = (listener: (...params: P[]) => void) => () => void
|
|
2
|
+
export interface Emitter<T, P = undefined> {
|
|
3
|
+
subscribe: EmitterSubscribe<P>
|
|
4
|
+
subscribeToOtherEmitter: (emitter: Emitter<unknown>) => void
|
|
5
|
+
getSnapshot: () => T
|
|
6
|
+
getInitialSnapshot?: () => T
|
|
7
|
+
emit: (...params: P[]) => void
|
|
8
|
+
getSize: () => number
|
|
9
|
+
clear: () => void
|
|
10
|
+
contains: (listener: (...params: P[]) => void) => boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generics parameters are:
|
|
15
|
+
* T: Type of the state
|
|
16
|
+
* R: Type of the snapshot
|
|
17
|
+
* P: Type of the parameters
|
|
18
|
+
* @param getSnapshot
|
|
19
|
+
* @returns
|
|
20
|
+
*/
|
|
21
|
+
export function createEmitter<T, P = undefined>(getSnapshot: () => T, getInitialSnapshot?: () => T): Emitter<T, P> {
|
|
22
|
+
const listeners = new Set<(...params: P[]) => void>()
|
|
23
|
+
const otherCleaners: Array<() => void> = []
|
|
24
|
+
return {
|
|
25
|
+
clear: () => {
|
|
26
|
+
for (const cleaner of otherCleaners) {
|
|
27
|
+
cleaner()
|
|
28
|
+
}
|
|
29
|
+
listeners.clear()
|
|
30
|
+
},
|
|
31
|
+
subscribe: (listener) => {
|
|
32
|
+
listeners.add(listener)
|
|
33
|
+
return () => {
|
|
34
|
+
listeners.delete(listener)
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
emit: (...params) => {
|
|
38
|
+
for (const listener of listeners) {
|
|
39
|
+
listener(...params)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
contains: (listener) => listeners.has(listener),
|
|
43
|
+
getSnapshot,
|
|
44
|
+
getInitialSnapshot,
|
|
45
|
+
getSize: () => listeners.size,
|
|
46
|
+
subscribeToOtherEmitter(emitter) {
|
|
47
|
+
const clean = emitter.subscribe(() => {
|
|
48
|
+
this.emit()
|
|
49
|
+
})
|
|
50
|
+
otherCleaners.push(clean)
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import { Abort } from './common'
|
|
2
|
-
import type {
|
|
2
|
+
import type { SetStateCb, SetValue } from '../types'
|
|
3
3
|
|
|
4
|
-
export function isPromise(value: unknown): value is Promise<
|
|
4
|
+
export function isPromise<T>(value: unknown): value is Promise<T> {
|
|
5
5
|
return value instanceof Promise
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
export function isSetValueFunction<T>(value: SetValue<T>): value is Setter<T> {
|
|
7
|
+
|
|
8
|
+
export function isFunction<T extends (...args: unknown[]) => unknown>(value: unknown): value is T {
|
|
11
9
|
return typeof value === 'function'
|
|
12
10
|
}
|
|
13
|
-
export function isObject(value: unknown): value is Record<string, unknown> {
|
|
14
|
-
return typeof value === 'object' && value !== null
|
|
15
|
-
}
|
|
16
|
-
export function isRef<T>(value: unknown): value is Ref<T> {
|
|
17
|
-
return isObject(value) && value.isRef === true
|
|
18
|
-
}
|
|
19
11
|
|
|
20
12
|
export function isMap(value: unknown): value is Map<unknown, unknown> {
|
|
21
13
|
return value instanceof Map
|
|
@@ -35,7 +27,9 @@ export function isEqualBase<T>(valueA: T, valueB: T): boolean {
|
|
|
35
27
|
}
|
|
36
28
|
return !!Object.is(valueA, valueB)
|
|
37
29
|
}
|
|
38
|
-
|
|
30
|
+
export function isSetValueFunction<T>(value: SetValue<T>): value is SetStateCb<T> {
|
|
31
|
+
return typeof value === 'function'
|
|
32
|
+
}
|
|
39
33
|
export function isAbortError(value: unknown): value is DOMException {
|
|
40
34
|
return value instanceof DOMException && value.name === Abort.Error
|
|
41
35
|
}
|
|
@@ -43,3 +37,7 @@ export function isAbortError(value: unknown): value is DOMException {
|
|
|
43
37
|
export function isAnyOtherError(value: unknown): value is Error {
|
|
44
38
|
return value instanceof Error && value.name !== Abort.Error
|
|
45
39
|
}
|
|
40
|
+
|
|
41
|
+
export function isUndefined(value: unknown): value is undefined {
|
|
42
|
+
return value === undefined
|
|
43
|
+
}
|