muya 2.0.0-beta.3 → 2.0.0
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 +124 -195
- package/cjs/index.js +1 -1
- package/esm/create-state.js +1 -0
- package/esm/create.js +1 -1
- package/esm/debug/development-tools.js +1 -1
- package/esm/index.js +1 -1
- package/esm/scheduler.js +1 -0
- package/esm/select.js +1 -0
- package/esm/use-value.js +1 -0
- package/esm/utils/__tests__/is.test.js +1 -1
- package/esm/utils/common.js +1 -1
- package/esm/utils/is.js +1 -1
- package/package.json +12 -12
- package/src/__tests__/bench.test.tsx +3 -108
- package/src/__tests__/create.test.tsx +122 -70
- package/src/__tests__/scheduler.test.tsx +52 -0
- package/src/__tests__/select.test.tsx +127 -0
- package/src/__tests__/use-value.test.tsx +78 -0
- package/src/create-state.ts +50 -0
- package/src/create.ts +42 -73
- package/src/debug/development-tools.ts +18 -3
- package/src/index.ts +2 -1
- package/src/{utils/global-scheduler.ts → scheduler.ts} +9 -3
- package/src/select.ts +69 -0
- package/src/types.ts +57 -6
- package/src/use-value.ts +22 -0
- package/src/utils/__tests__/is.test.ts +24 -7
- package/src/utils/common.ts +35 -10
- package/src/utils/is.ts +5 -8
- package/types/create-state.d.ts +12 -0
- package/types/create.d.ts +6 -18
- package/types/debug/development-tools.d.ts +2 -9
- package/types/index.d.ts +2 -1
- package/types/{utils/scheduler.d.ts → scheduler.d.ts} +4 -1
- package/types/select.d.ts +10 -0
- package/types/types.d.ts +55 -5
- package/types/use-value.d.ts +2 -0
- package/types/utils/common.d.ts +6 -5
- package/types/utils/is.d.ts +3 -4
- package/esm/__tests__/create-async.test.js +0 -1
- package/esm/subscriber.js +0 -1
- package/esm/use.js +0 -1
- package/esm/utils/__tests__/context.test.js +0 -1
- package/esm/utils/__tests__/sub-memo.test.js +0 -1
- package/esm/utils/create-context.js +0 -1
- package/esm/utils/global-scheduler.js +0 -1
- package/esm/utils/scheduler.js +0 -1
- package/esm/utils/sub-memo.js +0 -1
- package/src/__tests__/create-async.test.ts +0 -88
- package/src/__tests__/subscriber.test.tsx +0 -89
- package/src/__tests__/use-async.test.tsx +0 -45
- package/src/__tests__/use.test.tsx +0 -125
- package/src/subscriber.ts +0 -165
- package/src/use.ts +0 -57
- package/src/utils/__tests__/context.test.ts +0 -198
- package/src/utils/__tests__/sub-memo.test.ts +0 -13
- package/src/utils/create-context.ts +0 -60
- package/src/utils/scheduler.ts +0 -59
- package/src/utils/sub-memo.ts +0 -49
- package/types/subscriber.d.ts +0 -25
- package/types/use.d.ts +0 -2
- package/types/utils/create-context.d.ts +0 -5
- package/types/utils/global-scheduler.d.ts +0 -5
- package/types/utils/sub-memo.d.ts +0 -7
package/src/create.ts
CHANGED
|
@@ -1,75 +1,57 @@
|
|
|
1
|
-
import { canUpdate,
|
|
2
|
-
import type { Emitter } from './utils/create-emitter'
|
|
3
|
-
import { createEmitter } from './utils/create-emitter'
|
|
1
|
+
import { canUpdate, handleAsyncUpdate } from './utils/common'
|
|
4
2
|
import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
3
|
+
import type { Cache, DefaultValue, IsEqual, SetValue, State } from './types'
|
|
4
|
+
import { createScheduler } from './scheduler'
|
|
5
|
+
import { subscribeToDevelopmentTools } from './debug/development-tools'
|
|
6
|
+
import { createState } from './create-state'
|
|
9
7
|
|
|
10
|
-
export const
|
|
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>
|
|
8
|
+
export const stateScheduler = createScheduler()
|
|
25
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Create state from a default value.
|
|
12
|
+
*/
|
|
26
13
|
export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = isEqualBase): State<T> {
|
|
27
14
|
const cache: Cache<T> = {}
|
|
28
15
|
|
|
29
16
|
function getValue(): T {
|
|
30
|
-
|
|
31
|
-
cache.current
|
|
17
|
+
try {
|
|
18
|
+
if (isUndefined(cache.current)) {
|
|
19
|
+
const value = isFunction(initialValue) ? initialValue() : initialValue
|
|
20
|
+
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, value)
|
|
21
|
+
cache.current = resolvedValue
|
|
22
|
+
}
|
|
23
|
+
return cache.current
|
|
24
|
+
} catch (error) {
|
|
25
|
+
cache.current = error as T
|
|
32
26
|
}
|
|
33
27
|
return cache.current
|
|
34
28
|
}
|
|
35
|
-
function resolveValue(value: SetValue<T>) {
|
|
36
|
-
const previous = getValue()
|
|
37
|
-
cache.current = isSetValueFunction(value) ? value(previous) : value
|
|
38
|
-
}
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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)
|
|
30
|
+
function setValue(value: SetValue<T>) {
|
|
31
|
+
if (cache.abortController) {
|
|
32
|
+
cache.abortController.abort()
|
|
57
33
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (isUndefined(final)) {
|
|
64
|
-
throw new Error('The value is undefined')
|
|
65
|
-
}
|
|
66
|
-
listener(final)
|
|
67
|
-
})
|
|
34
|
+
|
|
35
|
+
const previous = getValue()
|
|
36
|
+
const newValue = isSetValueFunction(value) ? value(previous) : value
|
|
37
|
+
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue)
|
|
38
|
+
cache.current = resolvedValue
|
|
68
39
|
}
|
|
69
|
-
state.emitter = createEmitter<T>(() => state())
|
|
70
|
-
state.id = generateId()
|
|
71
40
|
|
|
72
|
-
const
|
|
41
|
+
const state = createState<T>({
|
|
42
|
+
get: getValue,
|
|
43
|
+
destroy() {
|
|
44
|
+
getValue()
|
|
45
|
+
clearScheduler()
|
|
46
|
+
state.emitter.clear()
|
|
47
|
+
cache.current = undefined
|
|
48
|
+
},
|
|
49
|
+
set(value: SetValue<T>) {
|
|
50
|
+
stateScheduler.schedule(state.id, value)
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const clearScheduler = stateScheduler.add(state.id, {
|
|
73
55
|
onFinish() {
|
|
74
56
|
cache.current = getValue()
|
|
75
57
|
if (!canUpdate(cache, isEqual)) {
|
|
@@ -77,22 +59,9 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
|
|
|
77
59
|
}
|
|
78
60
|
state.emitter.emit()
|
|
79
61
|
},
|
|
80
|
-
onResolveItem:
|
|
62
|
+
onResolveItem: setValue,
|
|
81
63
|
})
|
|
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
64
|
|
|
65
|
+
subscribeToDevelopmentTools(state)
|
|
97
66
|
return state
|
|
98
67
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { GetState, State } from '../types'
|
|
2
|
+
import { isPromise, isState } from '../utils/is'
|
|
2
3
|
|
|
3
4
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4
5
|
// @ts-expect-error
|
|
@@ -19,7 +20,7 @@ interface SendOptions {
|
|
|
19
20
|
value: unknown
|
|
20
21
|
name: string
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
function sendToDevelopmentTools(options: SendOptions) {
|
|
23
24
|
if (!reduxDevelopmentTools) {
|
|
24
25
|
return
|
|
25
26
|
}
|
|
@@ -30,8 +31,22 @@ export function sendToDevelopmentTools(options: SendOptions) {
|
|
|
30
31
|
reduxDevelopmentTools.send(name, { value, type, message }, type)
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
function developmentToolsListener(name: string, type: StateType) {
|
|
34
35
|
return (value: unknown) => {
|
|
35
36
|
sendToDevelopmentTools({ name, type, value, message: 'update' })
|
|
36
37
|
}
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
export function subscribeToDevelopmentTools<T>(state: State<T> | GetState<T>) {
|
|
41
|
+
if (process.env.NODE_ENV === 'production') {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
let type: StateType = 'state'
|
|
45
|
+
|
|
46
|
+
if (!isState(state)) {
|
|
47
|
+
type = 'derived'
|
|
48
|
+
}
|
|
49
|
+
const name = state.stateName?.length ? state.stateName : `${type}(${state.id.toString()})`
|
|
50
|
+
sendToDevelopmentTools({ name, type, value: state.get(), message: 'initial' })
|
|
51
|
+
return state.listen(developmentToolsListener(name, type))
|
|
52
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export const THRESHOLD = 0.2
|
|
2
|
+
export const THRESHOLD_ITEMS = 10
|
|
3
|
+
export const RESCHEDULE_COUNT = 0
|
|
3
4
|
|
|
4
5
|
interface GlobalSchedulerItem<T> {
|
|
5
6
|
value: T
|
|
6
7
|
id: number
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export
|
|
10
|
+
export interface SchedulerOptions<T> {
|
|
11
|
+
readonly onResolveItem?: (item: T) => void
|
|
12
|
+
readonly onFinish: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createScheduler() {
|
|
10
16
|
const listeners = new Map<number, SchedulerOptions<unknown>>()
|
|
11
17
|
const batches = new Set<GlobalSchedulerItem<unknown>>()
|
|
12
18
|
|
package/src/select.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { stateScheduler } from './create'
|
|
2
|
+
import { createState } from './create-state'
|
|
3
|
+
import { subscribeToDevelopmentTools } from './debug/development-tools'
|
|
4
|
+
import type { Cache, GetState, IsEqual } from './types'
|
|
5
|
+
import { canUpdate, handleAsyncUpdate } from './utils/common'
|
|
6
|
+
import { isUndefined } from './utils/is'
|
|
7
|
+
|
|
8
|
+
type StateDependencies<T extends Array<unknown>> = {
|
|
9
|
+
[K in keyof T]: GetState<T[K]>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Selecting state from multiple states.
|
|
14
|
+
* It will create new state in read-only mode (without set).
|
|
15
|
+
*/
|
|
16
|
+
export function select<T = unknown, S extends Array<unknown> = []>(
|
|
17
|
+
states: StateDependencies<S>,
|
|
18
|
+
selector: (...values: S) => T,
|
|
19
|
+
isEqual?: IsEqual<T>,
|
|
20
|
+
): GetState<T> {
|
|
21
|
+
const cache: Cache<T> = {}
|
|
22
|
+
|
|
23
|
+
function computedValue(): T {
|
|
24
|
+
const values = states.map((state) => state.get()) as S
|
|
25
|
+
return selector(...values)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getValue(): T {
|
|
29
|
+
if (isUndefined(cache.current)) {
|
|
30
|
+
const newValue = computedValue()
|
|
31
|
+
cache.current = handleAsyncUpdate(cache, state.emitter.emit, newValue)
|
|
32
|
+
}
|
|
33
|
+
return cache.current
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cleanups: Array<() => void> = []
|
|
37
|
+
for (const dependencyState of states) {
|
|
38
|
+
const clean = dependencyState.emitter.subscribe(() => {
|
|
39
|
+
stateScheduler.schedule(state.id, null)
|
|
40
|
+
})
|
|
41
|
+
cleanups.push(clean)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const state = createState<T>({
|
|
45
|
+
destroy() {
|
|
46
|
+
for (const cleanup of cleanups) {
|
|
47
|
+
cleanup()
|
|
48
|
+
}
|
|
49
|
+
clearScheduler()
|
|
50
|
+
state.emitter.clear()
|
|
51
|
+
cache.current = undefined
|
|
52
|
+
},
|
|
53
|
+
get: getValue,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const clearScheduler = stateScheduler.add(state.id, {
|
|
57
|
+
onFinish() {
|
|
58
|
+
const newValue = computedValue()
|
|
59
|
+
cache.current = handleAsyncUpdate(cache, state.emitter.emit, newValue)
|
|
60
|
+
if (!canUpdate(cache, isEqual)) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
state.emitter.emit()
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
subscribeToDevelopmentTools(state)
|
|
68
|
+
return state
|
|
69
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,15 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
export type AnyFunction = (...args: any[]) => any
|
|
1
|
+
import type { Emitter } from './utils/create-emitter'
|
|
3
2
|
|
|
4
3
|
export type IsEqual<T = unknown> = (a: T, b: T) => boolean
|
|
5
|
-
export type SetStateCb<T> = (value: T) => Awaited<T>
|
|
4
|
+
export type SetStateCb<T> = (value: T | Awaited<T>) => Awaited<T>
|
|
6
5
|
export type SetValue<T> = SetStateCb<T> | Awaited<T>
|
|
7
6
|
export type DefaultValue<T> = T | (() => T)
|
|
8
|
-
export type Listener<T> = (listener: (value
|
|
7
|
+
export type Listener<T> = (listener: (value?: T) => void) => () => void
|
|
9
8
|
export interface Cache<T> {
|
|
10
9
|
current?: T
|
|
11
10
|
previous?: T
|
|
11
|
+
abortController?: AbortController
|
|
12
12
|
}
|
|
13
|
-
export type Callable<T> = () => T
|
|
14
13
|
|
|
15
|
-
export const EMPTY_SELECTOR = <T>(stateValue: T) => stateValue
|
|
14
|
+
export const EMPTY_SELECTOR = <T, S>(stateValue: T) => stateValue as unknown as S
|
|
15
|
+
|
|
16
|
+
export interface GetState<T> {
|
|
17
|
+
<S>(selector?: (stateValue: T) => S): undefined extends S ? T : S
|
|
18
|
+
/**
|
|
19
|
+
* Get the cached state value.
|
|
20
|
+
*/
|
|
21
|
+
get: () => T
|
|
22
|
+
/**
|
|
23
|
+
* Get the unique id of the state.
|
|
24
|
+
*/
|
|
25
|
+
id: number
|
|
26
|
+
/**
|
|
27
|
+
* Emitter to listen to changes with snapshots.
|
|
28
|
+
*/
|
|
29
|
+
emitter: Emitter<T>
|
|
30
|
+
/**
|
|
31
|
+
* Listen to changes in the state.
|
|
32
|
+
*/
|
|
33
|
+
listen: Listener<T>
|
|
34
|
+
/**
|
|
35
|
+
* Destroy / cleanup the state.
|
|
36
|
+
* Clean all listeners and make cache value undefined.
|
|
37
|
+
*/
|
|
38
|
+
destroy: () => void
|
|
39
|
+
/**
|
|
40
|
+
* Set the state name. For debugging purposes.
|
|
41
|
+
*/
|
|
42
|
+
withName: (name: string) => GetState<T>
|
|
43
|
+
/**
|
|
44
|
+
* Name of the state. For debugging purposes.
|
|
45
|
+
*/
|
|
46
|
+
stateName?: string
|
|
47
|
+
/**
|
|
48
|
+
* Select particular slice of the state.
|
|
49
|
+
* It will create "another" state in read-only mode (without set).
|
|
50
|
+
*/
|
|
51
|
+
select: <S>(selector: (state: T) => S, isEqual?: IsEqual<S>) => GetState<S>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface State<T> extends GetState<T> {
|
|
55
|
+
/**
|
|
56
|
+
* Setting new state value.
|
|
57
|
+
* It can be value or function that returns a value (similar to `setState` in React).
|
|
58
|
+
* If the state is initialized with async code, set will cancel the previous promise.
|
|
59
|
+
*/
|
|
60
|
+
set: (value: SetValue<T>) => void
|
|
61
|
+
/**
|
|
62
|
+
* Set the state name. For debugging purposes.
|
|
63
|
+
*/
|
|
64
|
+
withName: (name: string) => State<T>
|
|
65
|
+
isSet: true
|
|
66
|
+
}
|
package/src/use-value.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useDebugValue, useSyncExternalStore } from 'react'
|
|
2
|
+
import { EMPTY_SELECTOR, type GetState } from './types'
|
|
3
|
+
import { isError, isPromise } from './utils/is'
|
|
4
|
+
|
|
5
|
+
export function useValue<T, S>(state: GetState<T>, selector: (stateValue: T) => S = EMPTY_SELECTOR): undefined extends S ? T : S {
|
|
6
|
+
const { emitter } = state
|
|
7
|
+
const value = useSyncExternalStore<S>(
|
|
8
|
+
state.emitter.subscribe,
|
|
9
|
+
() => selector(emitter.getSnapshot()),
|
|
10
|
+
() => selector(emitter.getInitialSnapshot ? emitter.getInitialSnapshot() : emitter.getSnapshot()),
|
|
11
|
+
)
|
|
12
|
+
useDebugValue(value)
|
|
13
|
+
if (isPromise(value)) {
|
|
14
|
+
throw value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (isError(value)) {
|
|
18
|
+
throw value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return value as undefined extends S ? T : S
|
|
22
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPromise, isFunction, isSetValueFunction, isMap, isSet, isArray, isEqualBase,
|
|
1
|
+
import { create } from '../../create'
|
|
2
|
+
import { isPromise, isFunction, isSetValueFunction, isMap, isSet, isArray, isEqualBase, isUndefined, isState } from '../is'
|
|
3
3
|
|
|
4
4
|
describe('isPromise', () => {
|
|
5
5
|
it('should return true for a Promise', () => {
|
|
@@ -64,11 +64,28 @@ describe('isEqualBase', () => {
|
|
|
64
64
|
})
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
describe('
|
|
68
|
-
it('should return true for
|
|
69
|
-
|
|
67
|
+
describe('isUndefined', () => {
|
|
68
|
+
it('should return true for undefined', () => {
|
|
69
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
70
|
+
expect(isUndefined(undefined)).toBe(true)
|
|
70
71
|
})
|
|
71
|
-
it('should return false for a non-
|
|
72
|
-
expect(
|
|
72
|
+
it('should return false for a non-undefined', () => {
|
|
73
|
+
expect(isUndefined(123)).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('isState', () => {
|
|
78
|
+
it('should return true for a State real', () => {
|
|
79
|
+
const state = create(1)
|
|
80
|
+
expect(isState(state)).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should return true for a State with derived', () => {
|
|
84
|
+
const state = create(1)
|
|
85
|
+
const derived = state.select((v) => v)
|
|
86
|
+
expect(isState(derived)).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
it('should return false for a non-State', () => {
|
|
89
|
+
expect(isState(123)).toBe(false)
|
|
73
90
|
})
|
|
74
91
|
})
|
package/src/utils/common.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Cache, IsEqual } from '../types'
|
|
2
|
-
import { isUndefined } from './is'
|
|
2
|
+
import { isAbortError, isEqualBase, isPromise, isUndefined } from './is'
|
|
3
3
|
|
|
4
4
|
// eslint-disable-next-line no-shadow
|
|
5
5
|
export enum Abort {
|
|
@@ -7,14 +7,13 @@ export enum Abort {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface CancelablePromise<T> {
|
|
10
|
-
promise
|
|
10
|
+
promise: Promise<T>
|
|
11
11
|
controller?: AbortController
|
|
12
|
-
resolveInitialPromise?: (value: T) => void
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
14
|
* Cancelable promise function, return promise and controller
|
|
16
15
|
*/
|
|
17
|
-
|
|
16
|
+
function cancelablePromise<T>(promise: Promise<T>, previousController?: AbortController): CancelablePromise<T> {
|
|
18
17
|
if (previousController) {
|
|
19
18
|
previousController.abort()
|
|
20
19
|
}
|
|
@@ -32,12 +31,10 @@ export function cancelablePromise<T>(promise: Promise<T>, previousController?: A
|
|
|
32
31
|
return { promise: cancelable, controller }
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export function canUpdate<T>(cache: Cache<T>, isEqual: IsEqual<T> = (previous, next) => previous === next): boolean {
|
|
34
|
+
/**
|
|
35
|
+
* Check if the cache value is different from the previous value.
|
|
36
|
+
*/
|
|
37
|
+
export function canUpdate<T>(cache: Cache<T>, isEqual: IsEqual<T> = isEqualBase): boolean {
|
|
41
38
|
if (!isUndefined(cache.current)) {
|
|
42
39
|
if (!isUndefined(cache.previous) && isEqual(cache.current, cache.previous)) {
|
|
43
40
|
return false
|
|
@@ -46,3 +43,31 @@ export function canUpdate<T>(cache: Cache<T>, isEqual: IsEqual<T> = (previous, n
|
|
|
46
43
|
}
|
|
47
44
|
return true
|
|
48
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle async updates for `create` and `select`
|
|
49
|
+
*/
|
|
50
|
+
export function handleAsyncUpdate<T>(cache: Cache<T>, emit: () => void, value: T): T {
|
|
51
|
+
if (!isPromise(value)) {
|
|
52
|
+
return value
|
|
53
|
+
}
|
|
54
|
+
if (cache.abortController) {
|
|
55
|
+
cache.abortController.abort()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { promise, controller } = cancelablePromise(value, cache.abortController)
|
|
59
|
+
cache.abortController = controller
|
|
60
|
+
|
|
61
|
+
return promise
|
|
62
|
+
.then((result) => {
|
|
63
|
+
cache.current = result as T
|
|
64
|
+
emit()
|
|
65
|
+
})
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
if (isAbortError(error)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
cache.current = error as T
|
|
71
|
+
emit()
|
|
72
|
+
}) as T
|
|
73
|
+
}
|
package/src/utils/is.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import type { SetStateCb, SetValue, State } from '../types'
|
|
1
2
|
import { Abort } from './common'
|
|
2
|
-
import type { SetStateCb, SetValue } from '../types'
|
|
3
|
-
import type { State } from '../create'
|
|
4
3
|
|
|
5
4
|
export function isPromise<T>(value: unknown): value is Promise<T> {
|
|
6
5
|
return value instanceof Promise
|
|
@@ -35,16 +34,14 @@ export function isAbortError(value: unknown): value is DOMException {
|
|
|
35
34
|
return value instanceof DOMException && value.name === Abort.Error
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
export function
|
|
39
|
-
return value instanceof Error
|
|
37
|
+
export function isError(value: unknown): value is Error {
|
|
38
|
+
return value instanceof Error
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
export function isUndefined(value: unknown): value is undefined {
|
|
43
42
|
return value === undefined
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
// @ts-expect-error
|
|
49
|
-
return isFunction(value) && value.set !== undefined
|
|
45
|
+
export function isState<T>(value: unknown): value is State<T> {
|
|
46
|
+
return isFunction(value) && 'get' in value && 'set' in value && 'isSet' in value && value.isSet === true
|
|
50
47
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GetState, SetValue, State } from './types';
|
|
2
|
+
interface GetStateOptions<T> {
|
|
3
|
+
readonly get: () => T;
|
|
4
|
+
readonly set?: (value: SetValue<T>) => void;
|
|
5
|
+
readonly destroy: () => void;
|
|
6
|
+
}
|
|
7
|
+
type FullState<T> = GetStateOptions<T>['set'] extends undefined ? GetState<T> : State<T>;
|
|
8
|
+
/**
|
|
9
|
+
* This is just utility function to create state base data
|
|
10
|
+
*/
|
|
11
|
+
export declare function createState<T>(options: GetStateOptions<T>): FullState<T>;
|
|
12
|
+
export {};
|
package/types/create.d.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
add<T>(id: number, option: import("./utils/scheduler").SchedulerOptions<T>): () => void;
|
|
1
|
+
import type { DefaultValue, IsEqual, State } from './types';
|
|
2
|
+
export declare const stateScheduler: {
|
|
3
|
+
add<T>(id: number, option: import("./scheduler").SchedulerOptions<T>): () => void;
|
|
5
4
|
schedule<T>(id: number, value: T): void;
|
|
6
5
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
set: (value: SetValue<T>) => void;
|
|
11
|
-
emitter: Emitter<T>;
|
|
12
|
-
listen: Listener<T>;
|
|
13
|
-
destroy: () => void;
|
|
14
|
-
withName: (name: string) => RawState<T>;
|
|
15
|
-
stateName?: string;
|
|
16
|
-
}
|
|
17
|
-
export type State<T> = {
|
|
18
|
-
readonly [K in keyof RawState<T>]: RawState<T>[K];
|
|
19
|
-
} & Callable<T>;
|
|
6
|
+
/**
|
|
7
|
+
* Create state from a default value.
|
|
8
|
+
*/
|
|
20
9
|
export declare function create<T>(initialValue: DefaultValue<T>, isEqual?: IsEqual<T>): State<T>;
|
|
21
|
-
export {};
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
+
import type { GetState, State } from '../types';
|
|
1
2
|
export type StateType = 'state' | 'derived';
|
|
2
|
-
|
|
3
|
-
message?: string;
|
|
4
|
-
type: StateType;
|
|
5
|
-
value: unknown;
|
|
6
|
-
name: string;
|
|
7
|
-
}
|
|
8
|
-
export declare function sendToDevelopmentTools(options: SendOptions): void;
|
|
9
|
-
export declare function developmentToolsListener(name: string, type: StateType): (value: unknown) => void;
|
|
10
|
-
export {};
|
|
3
|
+
export declare function subscribeToDevelopmentTools<T>(state: State<T> | GetState<T>): (() => void) | undefined;
|
package/types/index.d.ts
CHANGED
|
@@ -5,4 +5,7 @@ export interface SchedulerOptions<T> {
|
|
|
5
5
|
readonly onResolveItem?: (item: T) => void;
|
|
6
6
|
readonly onFinish: () => void;
|
|
7
7
|
}
|
|
8
|
-
export declare function createScheduler
|
|
8
|
+
export declare function createScheduler(): {
|
|
9
|
+
add<T>(id: number, option: SchedulerOptions<T>): () => void;
|
|
10
|
+
schedule<T>(id: number, value: T): void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GetState, IsEqual } from './types';
|
|
2
|
+
type StateDependencies<T extends Array<unknown>> = {
|
|
3
|
+
[K in keyof T]: GetState<T[K]>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Selecting state from multiple states.
|
|
7
|
+
* It will create new state in read-only mode (without set).
|
|
8
|
+
*/
|
|
9
|
+
export declare function select<T = unknown, S extends Array<unknown> = []>(states: StateDependencies<S>, selector: (...values: S) => T, isEqual?: IsEqual<T>): GetState<T>;
|
|
10
|
+
export {};
|
package/types/types.d.ts
CHANGED
|
@@ -1,12 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Emitter } from './utils/create-emitter';
|
|
2
2
|
export type IsEqual<T = unknown> = (a: T, b: T) => boolean;
|
|
3
|
-
export type SetStateCb<T> = (value: T) => Awaited<T>;
|
|
3
|
+
export type SetStateCb<T> = (value: T | Awaited<T>) => Awaited<T>;
|
|
4
4
|
export type SetValue<T> = SetStateCb<T> | Awaited<T>;
|
|
5
5
|
export type DefaultValue<T> = T | (() => T);
|
|
6
|
-
export type Listener<T> = (listener: (value
|
|
6
|
+
export type Listener<T> = (listener: (value?: T) => void) => () => void;
|
|
7
7
|
export interface Cache<T> {
|
|
8
8
|
current?: T;
|
|
9
9
|
previous?: T;
|
|
10
|
+
abortController?: AbortController;
|
|
11
|
+
}
|
|
12
|
+
export declare const EMPTY_SELECTOR: <T, S>(stateValue: T) => S;
|
|
13
|
+
export interface GetState<T> {
|
|
14
|
+
<S>(selector?: (stateValue: T) => S): undefined extends S ? T : S;
|
|
15
|
+
/**
|
|
16
|
+
* Get the cached state value.
|
|
17
|
+
*/
|
|
18
|
+
get: () => T;
|
|
19
|
+
/**
|
|
20
|
+
* Get the unique id of the state.
|
|
21
|
+
*/
|
|
22
|
+
id: number;
|
|
23
|
+
/**
|
|
24
|
+
* Emitter to listen to changes with snapshots.
|
|
25
|
+
*/
|
|
26
|
+
emitter: Emitter<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Listen to changes in the state.
|
|
29
|
+
*/
|
|
30
|
+
listen: Listener<T>;
|
|
31
|
+
/**
|
|
32
|
+
* Destroy / cleanup the state.
|
|
33
|
+
* Clean all listeners and make cache value undefined.
|
|
34
|
+
*/
|
|
35
|
+
destroy: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Set the state name. For debugging purposes.
|
|
38
|
+
*/
|
|
39
|
+
withName: (name: string) => GetState<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Name of the state. For debugging purposes.
|
|
42
|
+
*/
|
|
43
|
+
stateName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Select particular slice of the state.
|
|
46
|
+
* It will create "another" state in read-only mode (without set).
|
|
47
|
+
*/
|
|
48
|
+
select: <S>(selector: (state: T) => S, isEqual?: IsEqual<S>) => GetState<S>;
|
|
49
|
+
}
|
|
50
|
+
export interface State<T> extends GetState<T> {
|
|
51
|
+
/**
|
|
52
|
+
* Setting new state value.
|
|
53
|
+
* It can be value or function that returns a value (similar to `setState` in React).
|
|
54
|
+
* If the state is initialized with async code, set will cancel the previous promise.
|
|
55
|
+
*/
|
|
56
|
+
set: (value: SetValue<T>) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Set the state name. For debugging purposes.
|
|
59
|
+
*/
|
|
60
|
+
withName: (name: string) => State<T>;
|
|
61
|
+
isSet: true;
|
|
10
62
|
}
|
|
11
|
-
export type Callable<T> = () => T;
|
|
12
|
-
export declare const EMPTY_SELECTOR: <T>(stateValue: T) => T;
|