muya 1.1.0 → 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.
Files changed (85) hide show
  1. package/README.md +201 -138
  2. package/cjs/index.js +1 -1
  3. package/esm/__tests__/create-async.test.js +1 -0
  4. package/esm/__tests__/test-utils.js +1 -0
  5. package/esm/create.js +1 -1
  6. package/esm/index.js +1 -1
  7. package/esm/subscriber.js +1 -0
  8. package/esm/types.js +1 -1
  9. package/esm/use.js +1 -0
  10. package/esm/utils/__tests__/context.test.js +1 -0
  11. package/esm/utils/__tests__/is.test.js +1 -0
  12. package/esm/utils/__tests__/shallow.test.js +1 -0
  13. package/esm/utils/__tests__/sub-memo.test.js +1 -0
  14. package/esm/utils/common.js +1 -0
  15. package/esm/utils/create-context.js +1 -0
  16. package/esm/utils/create-emitter.js +1 -0
  17. package/esm/utils/is.js +1 -0
  18. package/esm/utils/scheduler.js +1 -0
  19. package/esm/utils/shallow.js +1 -0
  20. package/esm/utils/sub-memo.js +1 -0
  21. package/package.json +1 -1
  22. package/packages/core/__tests__/bench.test.tsx +261 -0
  23. package/packages/core/__tests__/create-async.test.ts +88 -0
  24. package/packages/core/__tests__/create.test.tsx +107 -0
  25. package/packages/core/__tests__/test-utils.ts +40 -0
  26. package/packages/core/__tests__/use-async.test.tsx +44 -0
  27. package/packages/core/__tests__/use.test.tsx +76 -0
  28. package/packages/core/create.ts +67 -0
  29. package/packages/core/index.ts +4 -0
  30. package/packages/core/subscriber.ts +121 -0
  31. package/packages/core/types.ts +15 -0
  32. package/packages/core/use.ts +59 -0
  33. package/packages/core/utils/__tests__/context.test.ts +198 -0
  34. package/packages/core/utils/__tests__/is.test.ts +74 -0
  35. package/packages/core/utils/__tests__/shallow.test.ts +418 -0
  36. package/packages/core/utils/__tests__/sub-memo.test.ts +13 -0
  37. package/packages/core/utils/common.ts +48 -0
  38. package/packages/core/utils/create-context.ts +60 -0
  39. package/packages/core/utils/create-emitter.ts +53 -0
  40. package/packages/core/utils/is.ts +43 -0
  41. package/packages/core/utils/scheduler.ts +59 -0
  42. package/{src → packages/core/utils}/shallow.ts +3 -6
  43. package/packages/core/utils/sub-memo.ts +37 -0
  44. package/types/__tests__/test-utils.d.ts +20 -0
  45. package/types/create.d.ts +14 -21
  46. package/types/index.d.ts +2 -4
  47. package/types/subscriber.d.ts +25 -0
  48. package/types/types.d.ts +9 -65
  49. package/types/use.d.ts +2 -0
  50. package/types/utils/common.d.ts +15 -0
  51. package/types/utils/create-context.d.ts +5 -0
  52. package/types/utils/create-emitter.d.ts +20 -0
  53. package/types/utils/is.d.ts +11 -0
  54. package/types/utils/scheduler.d.ts +6 -0
  55. package/types/utils/sub-memo.d.ts +6 -0
  56. package/esm/common.js +0 -1
  57. package/esm/create-base-state.js +0 -1
  58. package/esm/create-emitter.js +0 -1
  59. package/esm/create-getter-state.js +0 -1
  60. package/esm/is.js +0 -1
  61. package/esm/merge.js +0 -1
  62. package/esm/select.js +0 -1
  63. package/esm/shallow.js +0 -1
  64. package/esm/use-state-value.js +0 -1
  65. package/src/common.ts +0 -28
  66. package/src/create-base-state.ts +0 -35
  67. package/src/create-emitter.ts +0 -24
  68. package/src/create-getter-state.ts +0 -19
  69. package/src/create.ts +0 -102
  70. package/src/index.ts +0 -6
  71. package/src/is.ts +0 -36
  72. package/src/merge.ts +0 -41
  73. package/src/select.ts +0 -33
  74. package/src/state.test.tsx +0 -647
  75. package/src/types.ts +0 -94
  76. package/src/use-state-value.ts +0 -29
  77. package/types/common.d.ts +0 -7
  78. package/types/create-base-state.d.ts +0 -10
  79. package/types/create-emitter.d.ts +0 -7
  80. package/types/create-getter-state.d.ts +0 -6
  81. package/types/is.d.ts +0 -10
  82. package/types/merge.d.ts +0 -2
  83. package/types/select.d.ts +0 -2
  84. package/types/use-state-value.d.ts +0 -10
  85. /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
+ })
@@ -0,0 +1,74 @@
1
+ import { Abort } from '../common'
2
+ import { isPromise, isFunction, isSetValueFunction, isMap, isSet, isArray, isEqualBase, isAbortError } from '../is'
3
+
4
+ describe('isPromise', () => {
5
+ it('should return true for a Promise', () => {
6
+ expect(isPromise(Promise.resolve())).toBe(true)
7
+ })
8
+ it('should return false for a non-Promise', () => {
9
+ expect(isPromise(123)).toBe(false)
10
+ })
11
+ })
12
+
13
+ describe('isFunction', () => {
14
+ it('should return true for a function', () => {
15
+ expect(isFunction(() => {})).toBe(true)
16
+ })
17
+ it('should return false for a non-function', () => {
18
+ expect(isFunction(123)).toBe(false)
19
+ })
20
+ })
21
+
22
+ describe('isSetValueFunction', () => {
23
+ it('should return true for a function', () => {
24
+ expect(isSetValueFunction(() => {})).toBe(true)
25
+ })
26
+ it('should return false for a non-function', () => {
27
+ expect(isSetValueFunction(123)).toBe(false)
28
+ })
29
+ })
30
+
31
+ describe('isMap', () => {
32
+ it('should return true for a Map', () => {
33
+ expect(isMap(new Map())).toBe(true)
34
+ })
35
+ it('should return false for a non-Map', () => {
36
+ expect(isMap(123)).toBe(false)
37
+ })
38
+ })
39
+
40
+ describe('isSet', () => {
41
+ it('should return true for a Set', () => {
42
+ expect(isSet(new Set())).toBe(true)
43
+ })
44
+ it('should return false for a non-Set', () => {
45
+ expect(isSet(123)).toBe(false)
46
+ })
47
+ })
48
+
49
+ describe('isArray', () => {
50
+ it('should return true for an array', () => {
51
+ expect(isArray([])).toBe(true)
52
+ })
53
+ it('should return false for a non-array', () => {
54
+ expect(isArray(123)).toBe(false)
55
+ })
56
+ })
57
+
58
+ describe('isEqualBase', () => {
59
+ it('should return true for equal values', () => {
60
+ expect(isEqualBase(1, 1)).toBe(true)
61
+ })
62
+ it('should return false for non-equal values', () => {
63
+ expect(isEqualBase(1, 2)).toBe(false)
64
+ })
65
+ })
66
+
67
+ describe('isAbortError', () => {
68
+ it('should return true for an AbortError', () => {
69
+ expect(isAbortError(new DOMException('', Abort.Error))).toBe(true)
70
+ })
71
+ it('should return false for a non-AbortError', () => {
72
+ expect(isAbortError(new DOMException('', 'Error'))).toBe(false)
73
+ })
74
+ })