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.
Files changed (89) hide show
  1. package/README.md +194 -216
  2. package/cjs/index.js +1 -1
  3. package/esm/__tests__/create-async.test.js +1 -0
  4. package/esm/create.js +1 -1
  5. package/esm/index.js +1 -1
  6. package/esm/subscriber.js +1 -0
  7. package/esm/types.js +1 -1
  8. package/esm/use.js +1 -0
  9. package/esm/utils/__tests__/context.test.js +1 -0
  10. package/esm/utils/__tests__/is.test.js +1 -0
  11. package/esm/utils/__tests__/sub-memo.test.js +1 -0
  12. package/esm/utils/common.js +1 -0
  13. package/esm/utils/create-context.js +1 -0
  14. package/esm/utils/create-emitter.js +1 -0
  15. package/esm/utils/is.js +1 -0
  16. package/esm/utils/scheduler.js +1 -0
  17. package/esm/utils/shallow.js +1 -0
  18. package/esm/utils/sub-memo.js +1 -0
  19. package/package.json +1 -4
  20. package/packages/core/__tests__/bench.test.tsx +261 -0
  21. package/packages/core/__tests__/create-async.test.ts +88 -0
  22. package/packages/core/__tests__/create.test.tsx +107 -0
  23. package/packages/core/__tests__/use-async.test.tsx +44 -0
  24. package/packages/core/__tests__/use.test.tsx +76 -0
  25. package/packages/core/create.ts +67 -0
  26. package/packages/core/index.ts +4 -0
  27. package/packages/core/subscriber.ts +121 -0
  28. package/packages/core/types.ts +15 -0
  29. package/packages/core/use.ts +59 -0
  30. package/packages/core/utils/__tests__/context.test.ts +198 -0
  31. package/{src → packages/core/utils}/__tests__/is.test.ts +1 -30
  32. package/packages/core/utils/__tests__/sub-memo.test.ts +13 -0
  33. package/packages/core/utils/common.ts +48 -0
  34. package/packages/core/utils/create-context.ts +60 -0
  35. package/packages/core/utils/create-emitter.ts +53 -0
  36. package/{src → packages/core/utils}/is.ts +11 -13
  37. package/packages/core/utils/scheduler.ts +59 -0
  38. package/{src → packages/core/utils}/shallow.ts +3 -3
  39. package/packages/core/utils/sub-memo.ts +37 -0
  40. package/types/create.d.ts +14 -21
  41. package/types/index.d.ts +2 -4
  42. package/types/subscriber.d.ts +25 -0
  43. package/types/types.d.ts +9 -65
  44. package/types/use.d.ts +2 -0
  45. package/types/utils/common.d.ts +15 -0
  46. package/types/utils/create-context.d.ts +5 -0
  47. package/types/utils/create-emitter.d.ts +20 -0
  48. package/types/{is.d.ts → utils/is.d.ts} +5 -6
  49. package/types/utils/scheduler.d.ts +6 -0
  50. package/types/utils/sub-memo.d.ts +6 -0
  51. package/esm/__tests__/common.test.js +0 -1
  52. package/esm/__tests__/is.test.js +0 -1
  53. package/esm/__tests__/merge.test.js +0 -1
  54. package/esm/__tests__/types.test.js +0 -1
  55. package/esm/common.js +0 -1
  56. package/esm/create-base-state.js +0 -1
  57. package/esm/create-emitter.js +0 -1
  58. package/esm/create-getter-state.js +0 -1
  59. package/esm/is.js +0 -1
  60. package/esm/merge.js +0 -1
  61. package/esm/select.js +0 -1
  62. package/esm/shallow.js +0 -1
  63. package/esm/use-state-value.js +0 -1
  64. package/src/__tests__/common.test.ts +0 -63
  65. package/src/__tests__/create.test.tsx +0 -84
  66. package/src/__tests__/merge.test.ts +0 -78
  67. package/src/__tests__/state.test.tsx +0 -619
  68. package/src/__tests__/types.test.ts +0 -17
  69. package/src/common.ts +0 -60
  70. package/src/create-base-state.ts +0 -31
  71. package/src/create-emitter.ts +0 -24
  72. package/src/create-getter-state.ts +0 -18
  73. package/src/create.ts +0 -127
  74. package/src/index.ts +0 -6
  75. package/src/merge.ts +0 -38
  76. package/src/select.ts +0 -33
  77. package/src/types.ts +0 -94
  78. package/src/use-state-value.ts +0 -32
  79. package/types/common.d.ts +0 -17
  80. package/types/create-base-state.d.ts +0 -10
  81. package/types/create-emitter.d.ts +0 -7
  82. package/types/create-getter-state.d.ts +0 -6
  83. package/types/merge.d.ts +0 -4
  84. package/types/select.d.ts +0 -2
  85. package/types/use-state-value.d.ts +0 -10
  86. /package/esm/{__tests__ → utils/__tests__}/shallow.test.js +0 -0
  87. /package/{src → packages/core}/__tests__/test-utils.ts +0 -0
  88. /package/{src → packages/core/utils}/__tests__/shallow.test.ts +0 -0
  89. /package/types/{shallow.d.ts → utils/shallow.d.ts} +0 -0
@@ -0,0 +1 @@
1
+ import{subscriber as c}from"../subscriber";const r=new WeakMap;function s(t){return{call(){const e=r.get(t);if(e)return e.count++,e.returnType;const n={count:1,returnType:c(t)};return r.set(t,n),n.returnType},destroy(){const e=r.get(t);e&&(e.count--,e.count===0&&(e.returnType.destroy(),r.delete(t)))}}}export{s as subMemo};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muya",
3
- "version": "1.0.3",
3
+ "version": "2.0.0-beta.1",
4
4
  "author": "samuel.gjabel@gmail.com",
5
5
  "description": "👀 Another React state management library",
6
6
  "license": "MIT",
@@ -22,9 +22,6 @@
22
22
  "redux",
23
23
  "zustand"
24
24
  ],
25
- "dependencies": {
26
- "use-sync-external-store": "1.2.2"
27
- },
28
25
  "peerDependencies": {
29
26
  "use-sync-external-store": ">=1.2.0",
30
27
  "react": ">=18.0.0"
@@ -0,0 +1,261 @@
1
+ /* eslint-disable unicorn/consistent-function-scoping */
2
+ /* eslint-disable no-console */
3
+ import { act, renderHook } from '@testing-library/react-hooks'
4
+ import { useStore, create as zustand } from 'zustand'
5
+ import { useEffect, useState } from 'react'
6
+ import { use } from '../use'
7
+ import { atom, useAtom } from 'jotai'
8
+ import { create } from '../create'
9
+
10
+ function renderPerfHook<T>(hook: () => T, getValue: (data: T) => number, toBe: number) {
11
+ let onResolve = (_value: number) => {}
12
+ const resolvePromise = new Promise<number>((resolve) => {
13
+ onResolve = resolve
14
+ })
15
+ const start = performance.now()
16
+ const { result, waitFor } = renderHook(() => {
17
+ const data = hook()
18
+ const count = getValue(data)
19
+ useEffect(() => {
20
+ if (count === toBe) {
21
+ const end = performance.now()
22
+ onResolve(end - start)
23
+ }
24
+ }, [count])
25
+ return data
26
+ })
27
+ return { result, waitFor, resolvePromise }
28
+ }
29
+
30
+ describe('benchmarks comparison measure', () => {
31
+ const reRendersBefore = jest.fn()
32
+
33
+ beforeEach(() => {
34
+ jest.clearAllMocks()
35
+ })
36
+ const counts = [1000, 10_000]
37
+ for (const count of counts) {
38
+ describe(`Count ${count}`, () => {
39
+ it(`should benchmark ${count} muya first run - idk slow`, async () => {
40
+ const state = create(1)
41
+ // let count = 0
42
+
43
+ const { result, resolvePromise } = renderPerfHook(
44
+ () => {
45
+ reRendersBefore()
46
+ return use(state)
47
+ },
48
+ (data) => data,
49
+ count - 1,
50
+ )
51
+
52
+ for (let index = 0; index < count; index++) {
53
+ act(() => {
54
+ state.set(index)
55
+ })
56
+ }
57
+
58
+ const time = await resolvePromise
59
+ expect(result.current).toBe(count - 1)
60
+ console.log('Time', time)
61
+ console.log('Renders', reRendersBefore.mock.calls.length)
62
+ })
63
+ it(`should benchmark jotai ${count}`, async () => {
64
+ const state = atom(1)
65
+
66
+ const { result, resolvePromise } = renderPerfHook(
67
+ () => {
68
+ reRendersBefore()
69
+ return useAtom(state)
70
+ },
71
+ (data) => data[0],
72
+ count - 1,
73
+ )
74
+
75
+ for (let index = 0; index < count; index++) {
76
+ act(() => {
77
+ result.current[1](index)
78
+ })
79
+ }
80
+
81
+ const time = await resolvePromise
82
+ expect(result.current[0]).toBe(count - 1)
83
+ console.log('Time', time)
84
+ console.log('Renders', reRendersBefore.mock.calls.length)
85
+ })
86
+ it(`should benchmark zustand ${count}`, async () => {
87
+ const state = zustand((_set) => ({ state: 1 }))
88
+ const { result, resolvePromise } = renderPerfHook(
89
+ () => {
90
+ reRendersBefore()
91
+ return useStore(state)
92
+ },
93
+ (data) => data as number,
94
+ count - 1,
95
+ )
96
+
97
+ for (let index = 0; index < count; index++) {
98
+ act(() => {
99
+ state.setState(index)
100
+ })
101
+ }
102
+
103
+ const time = await resolvePromise
104
+ expect(result.current).toBe(count - 1)
105
+ console.log('Time', time)
106
+ console.log('Renders', reRendersBefore.mock.calls.length)
107
+ })
108
+
109
+ it(`should benchmark react ${count}`, async () => {
110
+ const { result, resolvePromise } = renderPerfHook(
111
+ () => {
112
+ reRendersBefore()
113
+ return useState(1)
114
+ },
115
+ (data) => data[0],
116
+ count - 1,
117
+ )
118
+
119
+ for (let index = 0; index < count; index++) {
120
+ act(() => {
121
+ result.current[1](index)
122
+ })
123
+ }
124
+
125
+ const time = await resolvePromise
126
+ expect(result.current[0]).toBe(count - 1)
127
+ console.log('Time', time)
128
+ console.log('Renders', reRendersBefore.mock.calls.length)
129
+ })
130
+ it(`should benchmark ${count} muya`, async () => {
131
+ const state = create(1)
132
+ // let count = 0
133
+
134
+ const { result, resolvePromise } = renderPerfHook(
135
+ () => {
136
+ reRendersBefore()
137
+ return use(state)
138
+ },
139
+ (data) => data,
140
+ count - 1,
141
+ )
142
+
143
+ for (let index = 0; index < count; index++) {
144
+ act(() => {
145
+ state.set(index)
146
+ })
147
+ }
148
+
149
+ const time = await resolvePromise
150
+ expect(result.current).toBe(count - 1)
151
+ console.log('Time', time)
152
+ console.log('Renders', reRendersBefore.mock.calls.length)
153
+ })
154
+ })
155
+ }
156
+ })
157
+
158
+ describe('benchmarks comparison between others', () => {
159
+ const reRendersBefore = jest.fn()
160
+
161
+ beforeEach(() => {
162
+ jest.clearAllMocks()
163
+ })
164
+ // const count = 10000
165
+
166
+ const counts = [100]
167
+ for (const count of counts) {
168
+ describe(`Count ${count}`, () => {
169
+ it(`should benchmark jotai ${count}`, async () => {
170
+ const state = atom(1)
171
+ const start = performance.now()
172
+ const { result, waitFor } = renderHook(() => {
173
+ reRendersBefore()
174
+ return useAtom(state)
175
+ })
176
+
177
+ for (let index = 0; index < count; index++) {
178
+ act(() => {
179
+ result.current[1](index)
180
+ })
181
+ }
182
+
183
+ await waitFor(() => {
184
+ expect(result.current[0]).toBe(count - 1)
185
+ })
186
+
187
+ const end = performance.now()
188
+ console.log('Time', end - start)
189
+ console.log('Renders', reRendersBefore.mock.calls.length)
190
+ // expect(reRendersBefore).toHaveBeenCalledTimes(3)
191
+ })
192
+ it(`should benchmark zustand ${count}`, async () => {
193
+ const state = zustand((_set) => ({ state: 1 }))
194
+ const start = performance.now()
195
+ const { result, waitFor } = renderHook(() => {
196
+ reRendersBefore()
197
+ return useStore(state)
198
+ })
199
+
200
+ for (let index = 0; index < count; index++) {
201
+ act(() => {
202
+ state.setState(index)
203
+ })
204
+ }
205
+
206
+ await waitFor(() => {
207
+ expect(result.current).toBe(count - 1)
208
+ })
209
+
210
+ const end = performance.now()
211
+ console.log('Time', end - start)
212
+ console.log('Renders', reRendersBefore.mock.calls.length)
213
+ })
214
+
215
+ it(`should benchmark react ${count}`, async () => {
216
+ const start = performance.now()
217
+ const { result, waitFor } = renderHook(() => {
218
+ reRendersBefore()
219
+ return useState(1)
220
+ })
221
+
222
+ for (let index = 0; index < count; index++) {
223
+ act(() => {
224
+ result.current[1](index)
225
+ })
226
+ }
227
+
228
+ await waitFor(() => {
229
+ expect(result.current[0]).toBe(count - 1)
230
+ })
231
+
232
+ const end = performance.now()
233
+ console.log('Time', end - start)
234
+ console.log('Renders', reRendersBefore.mock.calls.length)
235
+ })
236
+ })
237
+ it(`should benchmark ${count} muya`, async () => {
238
+ const state = create(1)
239
+ const start = performance.now()
240
+ const { result, waitFor } = renderHook(() => {
241
+ reRendersBefore()
242
+ return use(state)
243
+ })
244
+
245
+ for (let index = 0; index < count; index++) {
246
+ act(() => {
247
+ state.set(index)
248
+ })
249
+ }
250
+
251
+ await waitFor(() => {
252
+ expect(result.current).toBe(count - 1)
253
+ })
254
+
255
+ const end = performance.now()
256
+ console.log('Time', end - start)
257
+ console.log('Renders', reRendersBefore.mock.calls.length)
258
+ // expect(reRendersBefore).toHaveBeenCalledTimes(2)
259
+ })
260
+ }
261
+ })
@@ -0,0 +1,88 @@
1
+ import { create } from '../create'
2
+ import { waitFor } from '@testing-library/react'
3
+ import { longPromise } from './test-utils'
4
+ import { isPromise } from '../utils/is'
5
+ import { subscriber } from '../subscriber'
6
+
7
+ describe('create', () => {
8
+ it('should subscribe to context and notified it with parameters', async () => {
9
+ const state1 = create(1)
10
+ const state2 = create(2)
11
+
12
+ function derivedNested() {
13
+ return state1() + state2()
14
+ }
15
+ async function derived(plus: number) {
16
+ return state1() + state2() + derivedNested() + plus
17
+ }
18
+
19
+ let updatesCounter = 0
20
+ const sub = subscriber(() => derived(10))
21
+ expect(isPromise(sub.emitter.getSnapshot())).toBe(true)
22
+ sub.listen(async () => {
23
+ updatesCounter++
24
+ })
25
+ expect(updatesCounter).toBe(0)
26
+ // check if there is not maximum call stack
27
+ expect(await sub()).toBe(16)
28
+
29
+ state1.set(2)
30
+ //
31
+ await waitFor(async () => {})
32
+ expect(await sub()).toBe(18)
33
+
34
+ expect(updatesCounter).toBe(4)
35
+ })
36
+
37
+ it('should async subscribe to context and notified it', async () => {
38
+ const state1 = create(1)
39
+ const state2 = create(Promise.resolve(2))
40
+
41
+ async function derivedNested() {
42
+ await longPromise()
43
+ return state1() + (await state2())
44
+ }
45
+ async function derived() {
46
+ return state1() + (await state2()) + (await derivedNested())
47
+ }
48
+
49
+ let updatesCounter = 0
50
+
51
+ const sub = subscriber(derived)
52
+
53
+ sub.listen(() => {
54
+ updatesCounter++
55
+ })
56
+
57
+ // check if there is not maximum call stack
58
+ sub()
59
+
60
+ // check if not assigned multiple times, but only once
61
+ expect(state1.emitter.getSize()).toBe(1)
62
+ expect(state2.emitter.getSize()).toBe(1)
63
+ expect(sub.emitter.getSize()).toBe(1)
64
+ state1.set(2)
65
+
66
+ await waitFor(async () => {
67
+ expect(await sub()).toBe(8)
68
+ expect(updatesCounter).toBe(5)
69
+ })
70
+
71
+ state2.set(3)
72
+
73
+ await waitFor(async () => {
74
+ expect(await sub()).toBe(10)
75
+ expect(updatesCounter).toBe(10)
76
+ })
77
+
78
+ expect(state1.emitter.getSize()).toBe(1)
79
+ expect(state2.emitter.getSize()).toBe(1)
80
+ expect(sub.emitter.getSize()).toBe(1)
81
+
82
+ sub.destroy()
83
+
84
+ expect(state1.emitter.getSize()).toBe(0)
85
+ expect(state2.emitter.getSize()).toBe(0)
86
+ expect(sub.emitter.getSize()).toBe(0)
87
+ })
88
+ })
@@ -0,0 +1,107 @@
1
+ import { create } from '../create'
2
+ import { waitFor } from '@testing-library/react'
3
+ import { subscriber } from '../subscriber'
4
+
5
+ describe('create', () => {
6
+ it('should get basic value states', async () => {
7
+ const state1 = create(1)
8
+ const state2 = create(2)
9
+ expect(state1()).toBe(1)
10
+ expect(state2()).toBe(2)
11
+
12
+ state1.set(2)
13
+ state2.set(3)
14
+
15
+ await waitFor(() => {
16
+ expect(state1()).toBe(2)
17
+ expect(state2()).toBe(3)
18
+ })
19
+ })
20
+ it('should get basic and derived value', async () => {
21
+ const state1 = create(1)
22
+ const state2 = create(2)
23
+
24
+ function derived1() {
25
+ return state1() + state2()
26
+ }
27
+
28
+ expect(state1()).toBe(1)
29
+ expect(state2()).toBe(2)
30
+ expect(derived1()).toBe(3)
31
+
32
+ state1.set(2)
33
+ state2.set(3)
34
+
35
+ await waitFor(() => {
36
+ expect(state1()).toBe(2)
37
+ expect(state2()).toBe(3)
38
+ expect(derived1()).toBe(5)
39
+ })
40
+ })
41
+ it('should subscribe to context and notified it', async () => {
42
+ const state1 = create(1)
43
+ const state2 = create(2)
44
+
45
+ function derivedNested() {
46
+ return state1() + state2()
47
+ }
48
+ function derived() {
49
+ return state1() + state2() + derivedNested()
50
+ }
51
+
52
+ let updatesCounter = 0
53
+
54
+ const sub = subscriber(derived)
55
+
56
+ sub.listen(() => {
57
+ updatesCounter++
58
+ })
59
+
60
+ // check if there is not maximum call stack
61
+ sub()
62
+ sub()
63
+ sub()
64
+
65
+ // check if not assigned multiple times, but only once
66
+ expect(state1.emitter.getSize()).toBe(1)
67
+ expect(state2.emitter.getSize()).toBe(1)
68
+ expect(sub.emitter.getSize()).toBe(1)
69
+ expect(sub.emitter.getSnapshot()).toBe(6)
70
+ state1.set(2)
71
+
72
+ await waitFor(() => {
73
+ expect(sub()).toBe(8)
74
+ expect(updatesCounter).toBe(1)
75
+ })
76
+
77
+ state2.set(3)
78
+
79
+ await waitFor(() => {
80
+ expect(sub()).toBe(10)
81
+ expect(updatesCounter).toBe(2)
82
+ })
83
+
84
+ expect(state1.emitter.getSize()).toBe(1)
85
+ expect(state2.emitter.getSize()).toBe(1)
86
+ expect(sub.emitter.getSize()).toBe(1)
87
+ expect(sub.emitter.getSnapshot()).toBe(10)
88
+
89
+ sub.destroy()
90
+
91
+ expect(state1.emitter.getSize()).toBe(0)
92
+ expect(state2.emitter.getSize()).toBe(0)
93
+ expect(sub.emitter.getSize()).toBe(0)
94
+ })
95
+ it('should subscribe and set snapshot', async () => {
96
+ const state = create(1)
97
+ const sub = subscriber(state)
98
+ sub()
99
+
100
+ expect(sub.emitter.getSnapshot()).toBe(1)
101
+
102
+ state.set(2)
103
+ await waitFor(() => {
104
+ expect(sub.emitter.getSnapshot()).toBe(2)
105
+ })
106
+ })
107
+ })
@@ -0,0 +1,44 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { renderHook } from '@testing-library/react-hooks'
3
+ import { create } from '../create'
4
+ import { use } from '../use'
5
+ import { Suspense } from 'react'
6
+ import { longPromise } from './test-utils'
7
+
8
+ describe('use-create', () => {
9
+ const reRendersBefore = jest.fn()
10
+
11
+ beforeEach(() => {
12
+ jest.clearAllMocks()
13
+ })
14
+
15
+ it('should test sub hook', async () => {
16
+ const userState = create({ name: 'John', age: 30 })
17
+ async function getDataWithUser() {
18
+ const result = await fetch('https://jsonplaceholder.typicode.com/todos/1')
19
+ const json = await result.json()
20
+ return { age: userState().age, ...json }
21
+ }
22
+
23
+ const suspenseFunction = jest.fn()
24
+ function Loading() {
25
+ suspenseFunction()
26
+ return <div>Loading...</div>
27
+ }
28
+
29
+ const { result, waitFor } = renderHook(
30
+ () => {
31
+ reRendersBefore()
32
+ const data = use(getDataWithUser)
33
+ return data
34
+ },
35
+ // @ts-expect-error
36
+ { wrapper: ({ children }) => <Suspense fallback={<Loading />}>{children}</Suspense> },
37
+ )
38
+
39
+ await longPromise(1000)
40
+ await waitFor(() => {})
41
+ expect(suspenseFunction).toHaveBeenCalledTimes(1)
42
+ expect(result.current).toEqual({ userId: 1, id: 1, title: 'delectus aut autem', completed: false, age: 30 })
43
+ })
44
+ })
@@ -0,0 +1,76 @@
1
+ /* eslint-disable @typescript-eslint/no-shadow */
2
+ /* eslint-disable no-shadow */
3
+ import { act, renderHook } from '@testing-library/react-hooks'
4
+ import { create } from '../create'
5
+ import { use } from '../use'
6
+ import { subscriber } from '../subscriber'
7
+ import { waitFor } from '@testing-library/react'
8
+
9
+ describe('use-create', () => {
10
+ const reRendersBefore = jest.fn()
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllMocks()
14
+ })
15
+
16
+ it('should test sub hook', async () => {
17
+ const state = create(1)
18
+
19
+ const sub = subscriber(state)
20
+ expect(sub()).toBe(1)
21
+
22
+ act(() => {
23
+ state.set(2)
24
+ })
25
+
26
+ await waitFor(() => {})
27
+ expect(sub()).toBe(2)
28
+ })
29
+ it('should test use hook', async () => {
30
+ const state = create(1)
31
+
32
+ const { result, waitFor } = renderHook(() => {
33
+ reRendersBefore()
34
+ return use(state)
35
+ })
36
+
37
+ state.set(2)
38
+
39
+ await waitFor(() => {})
40
+ expect(result.current).toBe(2)
41
+ expect(reRendersBefore).toHaveBeenCalledTimes(2)
42
+
43
+ state.set(3)
44
+
45
+ await waitFor(() => {})
46
+ expect(result.current).toBe(3)
47
+ expect(reRendersBefore).toHaveBeenCalledTimes(3)
48
+ })
49
+
50
+ it('should test derived state', async () => {
51
+ const state1 = create(1)
52
+ const state2 = create(2)
53
+
54
+ function derivedBefore(plusValue: number) {
55
+ return state1() + state2() + plusValue
56
+ }
57
+
58
+ function derived() {
59
+ return state1() + state2() + derivedBefore(10)
60
+ }
61
+
62
+ const { result, waitFor } = renderHook(() => {
63
+ reRendersBefore()
64
+ return use(derived)
65
+ })
66
+
67
+ act(() => {
68
+ state1.set(2)
69
+ state2.set(3)
70
+ })
71
+
72
+ await waitFor(() => {})
73
+ expect(result.current).toBe(20)
74
+ expect(reRendersBefore).toHaveBeenCalledTimes(2)
75
+ })
76
+ })
@@ -0,0 +1,67 @@
1
+ import { canUpdate, generateId } from './utils/common'
2
+ import type { Emitter } from './utils/create-emitter'
3
+ import { createEmitter } from './utils/create-emitter'
4
+ import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
5
+ import { createScheduler } from './utils/scheduler'
6
+ import type { Cache, Callable, DefaultValue, IsEqual, Listener, SetValue } from './types'
7
+ import { context } from './subscriber'
8
+
9
+ interface RawState<T> {
10
+ (): T
11
+ id: number
12
+ set: (value: SetValue<T>) => void
13
+ emitter: Emitter<T>
14
+ listen: Listener<T>
15
+ }
16
+
17
+ export type State<T> = {
18
+ readonly [K in keyof RawState<T>]: RawState<T>[K]
19
+ } & Callable<T>
20
+
21
+ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = isEqualBase): State<T> {
22
+ const cache: Cache<T> = {}
23
+
24
+ function getValue(): T {
25
+ if (isUndefined(cache.current)) {
26
+ cache.current = isFunction(initialValue) ? initialValue() : initialValue
27
+ }
28
+ return cache.current
29
+ }
30
+ function resolveValue(value: SetValue<T>) {
31
+ const previous = getValue()
32
+ cache.current = isSetValueFunction(value) ? value(previous) : value
33
+ }
34
+
35
+ const schedule = createScheduler<SetValue<T>>({
36
+ onFinish() {
37
+ cache.current = getValue()
38
+ if (!canUpdate(cache, isEqual)) {
39
+ return
40
+ }
41
+ state.emitter.emit()
42
+ },
43
+ onResolveItem: resolveValue,
44
+ })
45
+
46
+ const state: RawState<T> = function () {
47
+ const stateValue = getValue()
48
+ const ctx = context.use()
49
+ if (ctx && !state.emitter.contains(ctx.sub)) {
50
+ ctx.addEmitter(state.emitter)
51
+ }
52
+ return stateValue
53
+ }
54
+ state.listen = function (listener: (value: T) => void) {
55
+ return state.emitter.subscribe(() => {
56
+ const final = cache.current
57
+ if (isUndefined(final)) {
58
+ throw new Error('The value is undefined')
59
+ }
60
+ listener(final)
61
+ })
62
+ }
63
+ state.emitter = createEmitter<T>(() => state())
64
+ state.id = generateId()
65
+ state.set = schedule
66
+ return state
67
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types'
2
+ export { create } from './create'
3
+ export { use } from './use'
4
+ export { shallow } from './utils/shallow'