muya 2.0.0-beta.2 → 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/{packages/core → src}/__tests__/bench.test.tsx +3 -108
- package/src/__tests__/create.test.tsx +159 -0
- 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 +67 -0
- package/{packages/core → src}/debug/development-tools.ts +18 -3
- package/{packages/core → src}/index.ts +2 -1
- package/{packages/core/utils/global-scheduler.ts → src/scheduler.ts} +9 -3
- package/src/select.ts +69 -0
- package/src/types.ts +66 -0
- package/src/use-value.ts +22 -0
- package/{packages/core → src}/utils/__tests__/is.test.ts +24 -7
- package/{packages/core → src}/utils/common.ts +35 -10
- package/{packages/core → 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/packages/core/__tests__/create-async.test.ts +0 -88
- package/packages/core/__tests__/create.test.tsx +0 -107
- package/packages/core/__tests__/subscriber.test.tsx +0 -89
- package/packages/core/__tests__/use-async.test.tsx +0 -45
- package/packages/core/__tests__/use.test.tsx +0 -125
- package/packages/core/create.ts +0 -98
- package/packages/core/subscriber.ts +0 -165
- package/packages/core/types.ts +0 -15
- package/packages/core/use.ts +0 -57
- package/packages/core/utils/__tests__/context.test.ts +0 -198
- package/packages/core/utils/__tests__/sub-memo.test.ts +0 -13
- package/packages/core/utils/create-context.ts +0 -60
- package/packages/core/utils/scheduler.ts +0 -59
- package/packages/core/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/{packages/core → src}/__tests__/test-utils.ts +0 -0
- /package/{packages/core → src}/utils/__tests__/shallow.test.ts +0 -0
- /package/{packages/core → src}/utils/create-emitter.ts +0 -0
- /package/{packages/core → src}/utils/shallow.ts +0 -0
|
@@ -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
|
+
}
|
|
@@ -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
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Emitter } from './utils/create-emitter'
|
|
2
|
+
|
|
3
|
+
export type IsEqual<T = unknown> = (a: T, b: T) => boolean
|
|
4
|
+
export type SetStateCb<T> = (value: T | Awaited<T>) => Awaited<T>
|
|
5
|
+
export type SetValue<T> = SetStateCb<T> | Awaited<T>
|
|
6
|
+
export type DefaultValue<T> = T | (() => T)
|
|
7
|
+
export type Listener<T> = (listener: (value?: T) => void) => () => void
|
|
8
|
+
export interface Cache<T> {
|
|
9
|
+
current?: T
|
|
10
|
+
previous?: T
|
|
11
|
+
abortController?: AbortController
|
|
12
|
+
}
|
|
13
|
+
|
|
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
|
})
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
package/types/utils/common.d.ts
CHANGED
|
@@ -3,13 +3,14 @@ export declare enum Abort {
|
|
|
3
3
|
Error = "StateAbortError"
|
|
4
4
|
}
|
|
5
5
|
export interface CancelablePromise<T> {
|
|
6
|
-
promise
|
|
6
|
+
promise: Promise<T>;
|
|
7
7
|
controller?: AbortController;
|
|
8
|
-
resolveInitialPromise?: (value: T) => void;
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
10
|
+
* Check if the cache value is different from the previous value.
|
|
12
11
|
*/
|
|
13
|
-
export declare function cancelablePromise<T>(promise: Promise<T>, previousController?: AbortController): CancelablePromise<T>;
|
|
14
|
-
export declare function generateId(): number;
|
|
15
12
|
export declare function canUpdate<T>(cache: Cache<T>, isEqual?: IsEqual<T>): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Handle async updates for `create` and `select`
|
|
15
|
+
*/
|
|
16
|
+
export declare function handleAsyncUpdate<T>(cache: Cache<T>, emit: () => void, value: T): T;
|
package/types/utils/is.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { SetStateCb, SetValue } from '../types';
|
|
2
|
-
import type { State } from '../create';
|
|
1
|
+
import type { SetStateCb, SetValue, State } from '../types';
|
|
3
2
|
export declare function isPromise<T>(value: unknown): value is Promise<T>;
|
|
4
3
|
export declare function isFunction<T extends (...args: unknown[]) => unknown>(value: unknown): value is T;
|
|
5
4
|
export declare function isMap(value: unknown): value is Map<unknown, unknown>;
|
|
@@ -8,6 +7,6 @@ export declare function isArray(value: unknown): value is Array<unknown>;
|
|
|
8
7
|
export declare function isEqualBase<T>(valueA: T, valueB: T): boolean;
|
|
9
8
|
export declare function isSetValueFunction<T>(value: SetValue<T>): value is SetStateCb<T>;
|
|
10
9
|
export declare function isAbortError(value: unknown): value is DOMException;
|
|
11
|
-
export declare function
|
|
10
|
+
export declare function isError(value: unknown): value is Error;
|
|
12
11
|
export declare function isUndefined(value: unknown): value is undefined;
|
|
13
|
-
export declare function
|
|
12
|
+
export declare function isState<T>(value: unknown): value is State<T>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{create as r}from"../create";import{waitFor as c}from"@testing-library/react";import{longPromise as m}from"./test-utils";import{isPromise as u}from"../utils/is";import{subscriber as n}from"../subscriber";describe("create",()=>{it("should subscribe to context and notified it with parameters",async()=>{const t=r(1),i=r(2);function s(){return t()+i()}async function a(p){return t()+i()+s()+p}let o=0;const e=n(()=>a(10));expect(u(e.emitter.getSnapshot())).toBe(!0),e.listen(async()=>{o++}),expect(o).toBe(0),expect(await e()).toBe(16),t.set(2),await c(async()=>{}),expect(await e()).toBe(18),expect(o).toBe(4)}),it("should async subscribe to context and notified it",async()=>{const t=r(1),i=r(Promise.resolve(2));async function s(){return await m(),t()+await i()}async function a(){return t()+await i()+await s()}let o=0;const e=n(a);e.listen(()=>{o++}),e(),expect(t.emitter.getSize()).toBe(1),expect(i.emitter.getSize()).toBe(1),expect(e.emitter.getSize()).toBe(1),t.set(2),await c(async()=>{expect(await e()).toBe(8),expect(o).toBe(5)}),i.set(3),await c(async()=>{expect(await e()).toBe(10),expect(o).toBe(10)}),expect(t.emitter.getSize()).toBe(1),expect(i.emitter.getSize()).toBe(1),expect(e.emitter.getSize()).toBe(1),e.destroy(),expect(t.emitter.getSize()).toBe(0),expect(i.emitter.getSize()).toBe(0),expect(e.emitter.getSize()).toBe(0)})});
|
package/esm/subscriber.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{cancelablePromise as F,canUpdate as v,generateId as h}from"./utils/common";import{createContext as x}from"./utils/create-context";import{createEmitter as S}from"./utils/create-emitter";import"./debug/development-tools";import{createGlobalScheduler as C}from"./utils/global-scheduler";import{isAbortError as E,isEqualBase as R,isPromise as m,isUndefined as w}from"./utils/is";const d=C(),b=x(void 0);function U(p){const r={};let u=!1;const a=[],i={},s=S(()=>u?r.current:(u=!0,n()),()=>(u=!0,n()));async function l(){v(r,R)&&(i.controller&&i.controller.abort(),d.schedule(c,null))}const c=h(),f=d.add(c,{onFinish(){r.current=n(),s.emit()}}),T={addEmitter(e){const t=e.subscribe(l);a.push(t)},id:c,sub:l};function y(e){const t=F(e,i.controller);return i.controller=t.controller,t.promise?.then(o=>{r.current=o,s.emit()}).catch(o=>{E(o)||(r.current=o,s.emit())}),t.promise}const n=function(){const e=b.run(T,p);if(!m(e))return r.current=e,e;const t=b.wrap(()=>y(e))();m(t)&&t.catch(()=>null);const o=t;return r.current=o,o};return n.emitter=s,n.destroy=function(){for(const e of a)e();s.clear(),f()},n.id=c,n.listen=function(e){return s.subscribe(()=>{const t=r.current;if(w(t))throw new Error("The value is undefined");e(t)})},n.abort=function(){i.controller&&i.controller.abort()},n}export{b as context,U as subscriber};
|
package/esm/use.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{useDebugValue as u,useEffect as s,useRef as m}from"react";import{EMPTY_SELECTOR as a}from"./types";import{isAnyOtherError as c,isPromise as T}from"./utils/is";import{useSyncExternalStore as d}from"react";import{subMemo as p}from"./utils/sub-memo";const f=10,E=3;function R(t){const e=m({renders:0,startTime:performance.now()});s(()=>{e.current.renders++,!(performance.now()-e.current.startTime<f)&&(e.current.renders<E||(e.current.startTime=performance.now(),e.current.renders=0,console.warn(`Function ${t.name.length>0?t.name:t} seems to be not memoized, wrap the function to the useCallback or use global defined functions.`)))},[t])}function b(t,e=a){const n=p(t),o=n.call(),i=o.emitter.getInitialSnapshot??o.emitter.getSnapshot;s(()=>n.destroy,[t,n.destroy]);const r=d(o.emitter.subscribe,()=>e(o.emitter.getSnapshot()),()=>e(i()));if(u(r),T(r))throw r;if(c(r))throw n.destroy(),r;return r}export{b as use};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createContext as c}from"../create-context";import{longPromise as r}from"../../__tests__/test-utils";describe("context",()=>{it("should check context",()=>{const e=c({name:"John Doe"}),t=()=>{e.run({name:"Jane Doe"},()=>{expect(e.use()).toEqual({name:"Jane Doe"})})};expect(e.use()).toEqual({name:"John Doe"}),t(),expect(e.use()).toEqual({name:"John Doe"})}),it("should test async context",e=>{const t=c("empty"),o=async()=>new Promise(u=>setTimeout(u,10));t.run("outer",()=>{expect(t.use()).toEqual("outer"),setTimeout(t.wrap(async()=>{try{await o(),expect(t.use()).toEqual("outer"),s()}catch(u){e(u)}}),10),t.run("inner",()=>{expect(t.use()).toEqual("inner"),setTimeout(t.wrap(()=>{try{expect(t.use()).toEqual("inner"),s()}catch(u){e(u)}}),10)}),expect(t.use()).toEqual("outer")});let n=0;function s(){n+=1,n===2&&e()}}),it("should test async nested context",e=>{const t=c(0);t.run(1,()=>{expect(t.use()).toEqual(1),t.run(2,()=>{t.run(3,()=>{expect(t.use()).toEqual(3),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(3)}),10),expect(t.use()).toEqual(3)}),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(2)}),10),expect(t.use()).toEqual(2),t.run(3,()=>{expect(t.use()).toEqual(3),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(3),t.run(4,()=>{expect(t.use()).toEqual(4),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(4),e()}),10),expect(t.use()).toEqual(4)})}),10),expect(t.use()).toEqual(3)}),expect(t.use()).toEqual(2)}),expect(t.use()).toEqual(1)}),expect(t.use()).toEqual(0)}),it("should stress test context with async random code",async()=>{const t=c(0);for(let n=0;n<1e4;n++)t.run(n,()=>{expect(t.use()).toEqual(n)});const o=[];for(let n=0;n<1e4;n++)t.run(n,()=>{expect(t.use()).toEqual(n);const s=new Promise(u=>{setTimeout(t.wrap(()=>{expect(t.use()).toEqual(n),u(n)}),Math.random()*100)});o.push(s)});await Promise.all(o)}),it("should-test-default-value-with-ctx",async()=>{const e=c({counter:1});e.run({counter:10},async()=>{await r(10),expect(e.use()?.counter).toBe(10)}),e.run({counter:12},()=>{expect(e.use()?.counter).toBe(12)})}),it("should test nested context",()=>{const t=c({count:0});function o(){const n=t.use();expect(n?.count).toBe(0),t.run({count:1},()=>{const s=t.use();expect(s?.count).toBe(1),t.run({count:2},()=>{const u=t.use();expect(u?.count).toBe(2)})})}o(),o()}),it("should test nested context with async when promise is returned, but not waited",async()=>{const e=c({count:0});async function t(){await r(10);const n=e.use();expect(n?.count).toBe(1)}async function o(){await t(),e.wrap(t);const n=e.use();expect(n?.count).toBe(1)}e.run({count:1},o)})});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{subMemo as t}from"../sub-memo";describe("memo-fn",()=>{it("should create memo fn",()=>{function e(){return!0}const o=t(e);expect(o.call().emitter).toBeDefined()})});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{isPromise as i}from"./is";const a=Symbol("_");function f(s){const t=[];function o(){if(t.length===0)return s;const e=t.at(-1);return e===a?s:e}function u(e,n){t.push(e);const r=n();return i(r)?(async()=>{try{return await r}finally{t.pop()}})():(t.pop(),r)}function c(e){const n=o();return()=>{t.push(n);const r=e();return i(r)?(async()=>{try{return await r}finally{t.pop()}})():(t.pop(),r)}}return{run:u,use:o,wrap:c}}export{f as createContext};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{RESCHEDULE_COUNT as l,THRESHOLD as f,THRESHOLD_ITEMS as d}from"./scheduler";function m(){const t=new Map,r=new Set;let s=performance.now(),u=!1;function c(){const n=performance.now(),e=n-s,{size:o}=r;if(e<f&&o>0&&o<d){s=n,i();return}u||(u=!0,Promise.resolve().then(()=>{u=!1,s=performance.now(),i()}))}function i(){if(r.size===0)return;const n=new Set;for(const e of r){if(t.has(e.id)){n.add(e.id);const{onResolveItem:o}=t.get(e.id);o&&o(e.value)}r.delete(e)}if(r.size>l){c();return}for(const e of n)t.get(e)?.onFinish()}return{add(n,e){return t.set(n,e),()=>{t.delete(n)}},schedule(n,e){r.add({value:e,id:n}),c()}}}export{m as createGlobalScheduler};
|
package/esm/utils/scheduler.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const d=.2,T=10,S=0;function E(f){const n=new Set,{onResolveItem:r,onFinish:a}=f;let o=performance.now(),t=!1;function s(){const e=performance.now(),u=e-o,{size:c}=n;if(u<.2&&c>0&&c<10){o=e,i();return}t||(t=!0,Promise.resolve().then(()=>{t=!1,o=performance.now(),i()}))}function i(){if(n.size!==0){for(const e of n)r&&r(e),n.delete(e);if(n.size>0){s();return}a()}}function l(e){n.add(e),s()}return l}export{S as RESCHEDULE_COUNT,d as THRESHOLD,T as THRESHOLD_ITEMS,E as createScheduler};
|
package/esm/utils/sub-memo.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{subscriber as c}from"../subscriber";const n=new WeakMap;let r=0;function p(){return r}function o(){r++}function b(t){return r=0,{call(){const e=n.get(t);if(e)return e.count++,e.returnType;o();const u={count:1,returnType:c(t)};return n.set(t,u),u.returnType},destroy(){const e=n.get(t);e&&(e.count--,e.count===0&&(e.returnType.destroy(),n.delete(t)))}}}export{p as getDebugCacheCreation,b as subMemo};
|