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.
Files changed (71) hide show
  1. package/README.md +124 -195
  2. package/cjs/index.js +1 -1
  3. package/esm/create-state.js +1 -0
  4. package/esm/create.js +1 -1
  5. package/esm/debug/development-tools.js +1 -1
  6. package/esm/index.js +1 -1
  7. package/esm/scheduler.js +1 -0
  8. package/esm/select.js +1 -0
  9. package/esm/use-value.js +1 -0
  10. package/esm/utils/__tests__/is.test.js +1 -1
  11. package/esm/utils/common.js +1 -1
  12. package/esm/utils/is.js +1 -1
  13. package/package.json +12 -12
  14. package/{packages/core → src}/__tests__/bench.test.tsx +3 -108
  15. package/src/__tests__/create.test.tsx +159 -0
  16. package/src/__tests__/scheduler.test.tsx +52 -0
  17. package/src/__tests__/select.test.tsx +127 -0
  18. package/src/__tests__/use-value.test.tsx +78 -0
  19. package/src/create-state.ts +50 -0
  20. package/src/create.ts +67 -0
  21. package/{packages/core → src}/debug/development-tools.ts +18 -3
  22. package/{packages/core → src}/index.ts +2 -1
  23. package/{packages/core/utils/global-scheduler.ts → src/scheduler.ts} +9 -3
  24. package/src/select.ts +69 -0
  25. package/src/types.ts +66 -0
  26. package/src/use-value.ts +22 -0
  27. package/{packages/core → src}/utils/__tests__/is.test.ts +24 -7
  28. package/{packages/core → src}/utils/common.ts +35 -10
  29. package/{packages/core → src}/utils/is.ts +5 -8
  30. package/types/create-state.d.ts +12 -0
  31. package/types/create.d.ts +6 -18
  32. package/types/debug/development-tools.d.ts +2 -9
  33. package/types/index.d.ts +2 -1
  34. package/types/{utils/scheduler.d.ts → scheduler.d.ts} +4 -1
  35. package/types/select.d.ts +10 -0
  36. package/types/types.d.ts +55 -5
  37. package/types/use-value.d.ts +2 -0
  38. package/types/utils/common.d.ts +6 -5
  39. package/types/utils/is.d.ts +3 -4
  40. package/esm/__tests__/create-async.test.js +0 -1
  41. package/esm/subscriber.js +0 -1
  42. package/esm/use.js +0 -1
  43. package/esm/utils/__tests__/context.test.js +0 -1
  44. package/esm/utils/__tests__/sub-memo.test.js +0 -1
  45. package/esm/utils/create-context.js +0 -1
  46. package/esm/utils/global-scheduler.js +0 -1
  47. package/esm/utils/scheduler.js +0 -1
  48. package/esm/utils/sub-memo.js +0 -1
  49. package/packages/core/__tests__/create-async.test.ts +0 -88
  50. package/packages/core/__tests__/create.test.tsx +0 -107
  51. package/packages/core/__tests__/subscriber.test.tsx +0 -89
  52. package/packages/core/__tests__/use-async.test.tsx +0 -45
  53. package/packages/core/__tests__/use.test.tsx +0 -125
  54. package/packages/core/create.ts +0 -98
  55. package/packages/core/subscriber.ts +0 -165
  56. package/packages/core/types.ts +0 -15
  57. package/packages/core/use.ts +0 -57
  58. package/packages/core/utils/__tests__/context.test.ts +0 -198
  59. package/packages/core/utils/__tests__/sub-memo.test.ts +0 -13
  60. package/packages/core/utils/create-context.ts +0 -60
  61. package/packages/core/utils/scheduler.ts +0 -59
  62. package/packages/core/utils/sub-memo.ts +0 -49
  63. package/types/subscriber.d.ts +0 -25
  64. package/types/use.d.ts +0 -2
  65. package/types/utils/create-context.d.ts +0 -5
  66. package/types/utils/global-scheduler.d.ts +0 -5
  67. package/types/utils/sub-memo.d.ts +0 -7
  68. /package/{packages/core → src}/__tests__/test-utils.ts +0 -0
  69. /package/{packages/core → src}/utils/__tests__/shallow.test.ts +0 -0
  70. /package/{packages/core → src}/utils/create-emitter.ts +0 -0
  71. /package/{packages/core → src}/utils/shallow.ts +0 -0
@@ -8,7 +8,7 @@
8
8
  import { act, renderHook } from '@testing-library/react-hooks'
9
9
  import { useStore, create as zustand } from 'zustand'
10
10
  import { useEffect, useState } from 'react'
11
- import { use } from '../use'
11
+ import { useValue } from '../use-value'
12
12
  import { atom, useAtom } from 'jotai'
13
13
  import { create } from '../create'
14
14
 
@@ -48,7 +48,7 @@ describe('benchmarks comparison measure', () => {
48
48
  const { result, resolvePromise } = renderPerfHook(
49
49
  () => {
50
50
  reRendersBefore()
51
- return use(state)
51
+ return useValue(state)
52
52
  },
53
53
  (data) => data,
54
54
  count - 1,
@@ -139,7 +139,7 @@ describe('benchmarks comparison measure', () => {
139
139
  const { result, resolvePromise } = renderPerfHook(
140
140
  () => {
141
141
  reRendersBefore()
142
- return use(state)
142
+ return useValue(state)
143
143
  },
144
144
  (data) => data,
145
145
  count - 1,
@@ -159,108 +159,3 @@ describe('benchmarks comparison measure', () => {
159
159
  })
160
160
  }
161
161
  })
162
-
163
- describe('benchmarks comparison between others', () => {
164
- const reRendersBefore = jest.fn()
165
-
166
- beforeEach(() => {
167
- jest.clearAllMocks()
168
- })
169
- // const count = 10000
170
-
171
- const counts = [100]
172
- for (const count of counts) {
173
- describe(`Count ${count}`, () => {
174
- it(`should benchmark jotai ${count}`, async () => {
175
- const state = atom(1)
176
- const start = performance.now()
177
- const { result, waitFor } = renderHook(() => {
178
- reRendersBefore()
179
- return useAtom(state)
180
- })
181
-
182
- for (let index = 0; index < count; index++) {
183
- act(() => {
184
- result.current[1](index)
185
- })
186
- }
187
-
188
- await waitFor(() => {
189
- expect(result.current[0]).toBe(count - 1)
190
- })
191
-
192
- const end = performance.now()
193
- console.log('Time', end - start)
194
- console.log('Renders', reRendersBefore.mock.calls.length)
195
- // expect(reRendersBefore).toHaveBeenCalledTimes(3)
196
- })
197
- it(`should benchmark zustand ${count}`, async () => {
198
- const state = zustand((_set) => ({ state: 1 }))
199
- const start = performance.now()
200
- const { result, waitFor } = renderHook(() => {
201
- reRendersBefore()
202
- return useStore(state)
203
- })
204
-
205
- for (let index = 0; index < count; index++) {
206
- act(() => {
207
- state.setState(index)
208
- })
209
- }
210
-
211
- await waitFor(() => {
212
- expect(result.current).toBe(count - 1)
213
- })
214
-
215
- const end = performance.now()
216
- console.log('Time', end - start)
217
- console.log('Renders', reRendersBefore.mock.calls.length)
218
- })
219
-
220
- it(`should benchmark react ${count}`, async () => {
221
- const start = performance.now()
222
- const { result, waitFor } = renderHook(() => {
223
- reRendersBefore()
224
- return useState(1)
225
- })
226
-
227
- for (let index = 0; index < count; index++) {
228
- act(() => {
229
- result.current[1](index)
230
- })
231
- }
232
-
233
- await waitFor(() => {
234
- expect(result.current[0]).toBe(count - 1)
235
- })
236
-
237
- const end = performance.now()
238
- console.log('Time', end - start)
239
- console.log('Renders', reRendersBefore.mock.calls.length)
240
- })
241
- })
242
- it(`should benchmark ${count} muya`, async () => {
243
- const state = create(1)
244
- const start = performance.now()
245
- const { result, waitFor } = renderHook(() => {
246
- reRendersBefore()
247
- return use(state)
248
- })
249
-
250
- for (let index = 0; index < count; index++) {
251
- act(() => {
252
- state.set(index)
253
- })
254
- }
255
-
256
- await waitFor(() => {
257
- expect(result.current).toBe(count - 1)
258
- })
259
-
260
- const end = performance.now()
261
- console.log('Time', end - start)
262
- console.log('Renders', reRendersBefore.mock.calls.length)
263
- // expect(reRendersBefore).toHaveBeenCalledTimes(2)
264
- })
265
- }
266
- })
@@ -0,0 +1,159 @@
1
+ import { create } from '../create'
2
+ import { waitFor } from '@testing-library/react'
3
+ import { longPromise } from './test-utils'
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.get()).toBe(1)
10
+ expect(state2.get()).toBe(2)
11
+
12
+ state1.set(2)
13
+ state2.set(3)
14
+
15
+ await waitFor(() => {
16
+ expect(state1.get()).toBe(2)
17
+ expect(state2.get()).toBe(3)
18
+ })
19
+ })
20
+ it('should check if value is subscribed to the state', async () => {
21
+ const state = create(1)
22
+ const listener = jest.fn()
23
+ state.listen(listener)
24
+ state.set(2)
25
+ await waitFor(() => {
26
+ expect(listener).toHaveBeenCalledWith(2)
27
+ })
28
+ })
29
+
30
+ it('should check if value is unsubscribed from the state', async () => {
31
+ const state = create(1)
32
+ const listener = jest.fn()
33
+ const unsubscribe = state.listen(listener)
34
+ unsubscribe()
35
+ state.set(2)
36
+ await waitFor(() => {
37
+ expect(listener).not.toHaveBeenCalled()
38
+ })
39
+ })
40
+ it('should check change part of state, but is not equal', async () => {
41
+ const state = create({ count: 1, anotherCount: 1 }, (previous, next) => previous.anotherCount === next.anotherCount)
42
+ const listener = jest.fn()
43
+ state.listen(listener)
44
+ state.set((previous) => ({ ...previous, count: previous.count + 1 }))
45
+ await waitFor(() => {
46
+ expect(listener).not.toHaveBeenCalled()
47
+ })
48
+ })
49
+ it('should check change part of state, is not equal', async () => {
50
+ const state = create({ count: 1, anotherCount: 1 }, (previous, next) => previous.count === next.count)
51
+ const listener = jest.fn()
52
+ state.listen(listener)
53
+ state.set((previous) => ({ ...previous, count: previous.count + 1 }))
54
+ await waitFor(() => {
55
+ expect(listener).toHaveBeenCalledWith({ count: 2, anotherCount: 1 })
56
+ })
57
+ })
58
+
59
+ it('should initialize state with a function', () => {
60
+ const initialValue = jest.fn(() => 10)
61
+ const state = create(initialValue)
62
+ expect(initialValue).toHaveBeenCalled()
63
+ expect(state.get()).toBe(10)
64
+ })
65
+
66
+ it('should handle asynchronous state updates', async () => {
67
+ const state = create(0)
68
+ const listener = jest.fn()
69
+ state.listen(listener)
70
+ setTimeout(() => {
71
+ state.set(1)
72
+ }, 100)
73
+ await waitFor(() => {
74
+ expect(state.get()).toBe(1)
75
+ expect(listener).toHaveBeenCalledWith(1)
76
+ })
77
+ })
78
+
79
+ it('should notify multiple listeners', async () => {
80
+ const state = create('initial')
81
+ const listener1 = jest.fn()
82
+ const listener2 = jest.fn()
83
+ state.listen(listener1)
84
+ state.listen(listener2)
85
+ state.set('updated')
86
+ await waitFor(() => {
87
+ expect(listener1).toHaveBeenCalledWith('updated')
88
+ expect(listener2).toHaveBeenCalledWith('updated')
89
+ })
90
+ })
91
+
92
+ it('should not update if isEqual returns true', async () => {
93
+ const state = create(1, () => true)
94
+ const listener = jest.fn()
95
+ state.listen(listener)
96
+ state.set(2)
97
+ await waitFor(() => {
98
+ expect(listener).not.toHaveBeenCalled()
99
+ })
100
+ })
101
+
102
+ it('should clear state and listeners on destroy', async () => {
103
+ const state = create(1)
104
+ const listener = jest.fn()
105
+ state.listen(listener)
106
+ state.destroy()
107
+ state.set(2)
108
+ await waitFor(() => {})
109
+ expect(state.get()).toBe(1)
110
+ expect(listener).not.toHaveBeenCalledWith(2)
111
+ })
112
+
113
+ it('should create new get select state', async () => {
114
+ const state = create({ count: 1 })
115
+ const select = state.select((slice) => slice.count)
116
+ expect(select.get()).toBe(1)
117
+
118
+ state.set({ count: 2 })
119
+ await waitFor(() => {
120
+ expect(select.get()).toBe(2)
121
+ })
122
+ })
123
+
124
+ it('should create state with async value', async () => {
125
+ const state = create(() => longPromise(100))
126
+ await waitFor(() => {
127
+ expect(state.get()).toBe(0)
128
+ })
129
+ state.set(1)
130
+ await waitFor(() => {
131
+ expect(state.get()).toBe(1)
132
+ })
133
+ })
134
+ it('should create state with async value but will be cancelled by set value before it will resolve', async () => {
135
+ const state = create(() => longPromise(100))
136
+ state.set(2)
137
+ await waitFor(() => {
138
+ expect(state.get()).toBe(2)
139
+ })
140
+ })
141
+ it('should handle async select', async () => {
142
+ const state = create(0)
143
+ const asyncState = state.select(async (s) => {
144
+ await longPromise(100)
145
+ return s + 1
146
+ })
147
+ const listener = jest.fn()
148
+ asyncState.listen(listener)
149
+ await waitFor(() => {
150
+ expect(asyncState.get()).toBe(1)
151
+ expect(listener).toHaveBeenCalledWith(1)
152
+ })
153
+ state.set(1)
154
+ await waitFor(() => {
155
+ expect(asyncState.get()).toBe(2)
156
+ expect(listener).toHaveBeenCalledWith(2)
157
+ })
158
+ })
159
+ })
@@ -0,0 +1,52 @@
1
+ import { waitFor } from '@testing-library/react'
2
+ import { createScheduler } from '../scheduler'
3
+
4
+ describe('scheduler', () => {
5
+ it('should test scheduler by id', async () => {
6
+ const scheduler = createScheduler()
7
+
8
+ const id = 1
9
+ const value = 2
10
+ const callback = jest.fn()
11
+ scheduler.add(id, {
12
+ onFinish: callback,
13
+ })
14
+ scheduler.schedule(id, value)
15
+ await waitFor(() => {
16
+ expect(callback).toHaveBeenCalled()
17
+ })
18
+ })
19
+ it('should test scheduler with multiple ids', async () => {
20
+ const ids = [1, 2, 3]
21
+ const scheduler = createScheduler()
22
+ const callbacks: unknown[] = []
23
+ for (const id of ids) {
24
+ const callback = jest.fn()
25
+ scheduler.add(id, {
26
+ onFinish: callback,
27
+ })
28
+ callbacks.push(callback)
29
+ }
30
+ scheduler.schedule(1, 2)
31
+ await waitFor(() => {
32
+ expect(callbacks[0]).toHaveBeenCalled()
33
+ expect(callbacks[1]).not.toHaveBeenCalled()
34
+ expect(callbacks[2]).not.toHaveBeenCalled()
35
+ })
36
+ jest.clearAllMocks()
37
+ scheduler.schedule(2, 2)
38
+ await waitFor(() => {
39
+ expect(callbacks[0]).not.toHaveBeenCalled()
40
+ expect(callbacks[1]).toHaveBeenCalled()
41
+ expect(callbacks[2]).not.toHaveBeenCalled()
42
+ })
43
+
44
+ jest.clearAllMocks()
45
+ scheduler.schedule(3, 2)
46
+ await waitFor(() => {
47
+ expect(callbacks[0]).not.toHaveBeenCalled()
48
+ expect(callbacks[1]).not.toHaveBeenCalled()
49
+ expect(callbacks[2]).toHaveBeenCalled()
50
+ })
51
+ })
52
+ })
@@ -0,0 +1,127 @@
1
+ import { create } from '../create'
2
+ import { select } from '../select'
3
+ import { waitFor } from '@testing-library/react'
4
+ import { longPromise } from './test-utils'
5
+
6
+ describe('select', () => {
7
+ it('should derive state from a single dependency', async () => {
8
+ const state = create(1)
9
+ const selectedState = select([state], (value) => value * 2)
10
+ expect(selectedState.get()).toBe(2)
11
+ state.set(2)
12
+ await waitFor(() => {})
13
+ expect(selectedState.get()).toBe(4)
14
+ })
15
+
16
+ it('should derive state from multiple dependencies', async () => {
17
+ const state1 = create(1)
18
+ const state2 = create(2)
19
+ const selectedState = select([state1, state2], (a, b) => a + b)
20
+ expect(selectedState.get()).toBe(3)
21
+ state1.set(2)
22
+ await waitFor(() => {})
23
+ expect(selectedState.get()).toBe(4)
24
+ state2.set(3)
25
+ await waitFor(() => {})
26
+ expect(selectedState.get()).toBe(5)
27
+ })
28
+
29
+ it('should notify listeners when derived state changes', async () => {
30
+ const state = create(1)
31
+ const selectedState = select([state], (value) => value * 2)
32
+ const listener = jest.fn()
33
+ selectedState.listen(listener)
34
+ state.set(2)
35
+ await waitFor(() => {
36
+ expect(selectedState.get()).toBe(4)
37
+ expect(listener).toHaveBeenCalledWith(4)
38
+ })
39
+ })
40
+
41
+ it('should not notify listeners if isEqual returns true', async () => {
42
+ const state = create(1)
43
+ const selectedState = select(
44
+ [state],
45
+ (value) => value * 2,
46
+ () => true,
47
+ )
48
+ const listener = jest.fn()
49
+ selectedState.listen(listener)
50
+ state.set(2)
51
+ await waitFor(() => {
52
+ expect(listener).not.toHaveBeenCalled()
53
+ })
54
+ })
55
+
56
+ it('should destroy select state properly', async () => {
57
+ const state = create(1)
58
+ const selectedState = select([state], (value) => value * 2)
59
+ const listener = jest.fn()
60
+ selectedState.listen(listener)
61
+ selectedState.destroy()
62
+ state.set(2)
63
+ await waitFor(() => {})
64
+ // there are no listeners to notify, so it return 4 as value is computed again, but internally it's destroyed and undefined
65
+ // so it works as expected
66
+ expect(selectedState.get()).toBe(4)
67
+ expect(listener).not.toHaveBeenCalled()
68
+ })
69
+ it('should handle async updates', async () => {
70
+ const state1 = create(1)
71
+ const state2 = create(2)
72
+ const selectedState = select([state1, state2], async (a, b) => {
73
+ await longPromise()
74
+ return a + b
75
+ })
76
+ const listener = jest.fn()
77
+ selectedState.listen(listener)
78
+ state1.set(2)
79
+ state2.set(3)
80
+ await waitFor(() => {
81
+ expect(selectedState.get()).toBe(5)
82
+ expect(listener).toHaveBeenCalledWith(5)
83
+ })
84
+ })
85
+ it('should handle async updates with async state', async () => {
86
+ const state = create(longPromise(100))
87
+ const selectedState = select([state], async (value) => {
88
+ await longPromise(100)
89
+ return (await value) + 1
90
+ })
91
+ const listener = jest.fn()
92
+ selectedState.listen(listener)
93
+ await waitFor(() => {
94
+ expect(selectedState.get()).toBe(1)
95
+ expect(listener).toHaveBeenCalledWith(1)
96
+ })
97
+ })
98
+ it('should handle sync state updates when one of par is changed', async () => {
99
+ const state1Atom = create(0)
100
+ const state2Atom = create(0)
101
+ const state3Atom = create(0)
102
+
103
+ const sumState = select([state1Atom, state2Atom, state3Atom], (a, b, c) => a + b + c)
104
+
105
+ const listener = jest.fn()
106
+ sumState.listen(listener)
107
+ expect(sumState.get()).toBe(0)
108
+
109
+ state1Atom.set(1)
110
+ await waitFor(() => {
111
+ expect(sumState.get()).toBe(1)
112
+ expect(listener).toHaveBeenCalledWith(1)
113
+ })
114
+
115
+ state2Atom.set(1)
116
+ await waitFor(() => {
117
+ expect(sumState.get()).toBe(2)
118
+ expect(listener).toHaveBeenCalledWith(2)
119
+ })
120
+
121
+ state3Atom.set(1)
122
+ await waitFor(() => {
123
+ expect(sumState.get()).toBe(3)
124
+ expect(listener).toHaveBeenCalledWith(3)
125
+ })
126
+ })
127
+ })
@@ -0,0 +1,78 @@
1
+ import { renderHook, act } from '@testing-library/react-hooks'
2
+ import { create } from '../create'
3
+ import { useValue } from '../use-value'
4
+ import { waitFor } from '@testing-library/react'
5
+
6
+ describe('useValue', () => {
7
+ it('should get the initial state value', () => {
8
+ const state = create(1)
9
+ const { result } = renderHook(() => useValue(state))
10
+ expect(result.current).toBe(1)
11
+ })
12
+
13
+ it('should get the initial state value', () => {
14
+ const state = create(1)
15
+ const { result } = renderHook(() => state())
16
+ expect(result.current).toBe(1)
17
+ })
18
+
19
+ it('should update when the state changes', async () => {
20
+ const state = create(1)
21
+ const { result } = renderHook(() => useValue(state))
22
+ act(() => {
23
+ state.set(2)
24
+ })
25
+ await waitFor(() => {
26
+ expect(result.current).toBe(2)
27
+ })
28
+ })
29
+
30
+ it('should use a selector function', () => {
31
+ const state = create({ count: 1 })
32
+ const { result } = renderHook(() => useValue(state, (s) => s.count))
33
+ expect(result.current).toBe(1)
34
+ })
35
+
36
+ it('should handle errors thrown from state', () => {
37
+ const error = new Error('Test error')
38
+ const state = create(() => {
39
+ throw error
40
+ })
41
+ const { result } = renderHook(() => useValue(state))
42
+ expect(result.error).toBe(error)
43
+ })
44
+
45
+ it('should handle promises returned from state suspense', async () => {
46
+ const promise = Promise.resolve(1)
47
+ const state = create(() => promise)
48
+ const renders = jest.fn()
49
+ const { result } = renderHook(() => {
50
+ renders()
51
+ return useValue(state)
52
+ })
53
+ await waitFor(() => {})
54
+ expect(result.current).toBe(1)
55
+ expect(renders).toHaveBeenCalledTimes(2)
56
+ })
57
+
58
+ it('should unsubscribe on unmount', async () => {
59
+ const state = create(1)
60
+ const renders = jest.fn()
61
+ const { unmount } = renderHook(() => {
62
+ renders()
63
+ const value = useValue(state)
64
+ return value
65
+ })
66
+ act(() => {
67
+ state.set(2)
68
+ })
69
+ await waitFor(() => {})
70
+ expect(renders).toHaveBeenCalledTimes(2)
71
+ unmount()
72
+ act(() => {
73
+ state.set(3)
74
+ })
75
+ await waitFor(() => {})
76
+ expect(renders).toHaveBeenCalledTimes(2)
77
+ })
78
+ })
@@ -0,0 +1,50 @@
1
+ import { select } from './select'
2
+ import type { GetState, SetValue, State } from './types'
3
+ import { useValue } from './use-value'
4
+ import { createEmitter } from './utils/create-emitter'
5
+ import { isEqualBase } from './utils/is'
6
+
7
+ interface GetStateOptions<T> {
8
+ readonly get: () => T
9
+ readonly set?: (value: SetValue<T>) => void
10
+ readonly destroy: () => void
11
+ }
12
+
13
+ let stateId = 0
14
+ function getStateId() {
15
+ return stateId++
16
+ }
17
+
18
+ type FullState<T> = GetStateOptions<T>['set'] extends undefined ? GetState<T> : State<T>
19
+ /**
20
+ * This is just utility function to create state base data
21
+ */
22
+ export function createState<T>(options: GetStateOptions<T>): FullState<T> {
23
+ const { get, destroy, set } = options
24
+ const isSet = !!set
25
+
26
+ const state: FullState<T> = function (selector) {
27
+ // eslint-disable-next-line react-hooks/rules-of-hooks
28
+ return useValue(state, selector)
29
+ }
30
+ state.isSet = isSet as true
31
+ state.id = getStateId()
32
+ state.emitter = createEmitter<T>(get)
33
+ state.destroy = destroy
34
+ state.listen = function (listener) {
35
+ return this.emitter.subscribe(() => {
36
+ listener(get())
37
+ })
38
+ }
39
+ state.withName = function (name) {
40
+ this.stateName = name
41
+ return this
42
+ }
43
+ state.select = function (selector, isSelectorEqual = isEqualBase) {
44
+ return select([state], selector, isSelectorEqual)
45
+ }
46
+ state.get = get
47
+ state.set = set as State<T>['set']
48
+
49
+ return state
50
+ }
package/src/create.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { canUpdate, handleAsyncUpdate } from './utils/common'
2
+ import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
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'
7
+
8
+ export const stateScheduler = createScheduler()
9
+
10
+ /**
11
+ * Create state from a default value.
12
+ */
13
+ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = isEqualBase): State<T> {
14
+ const cache: Cache<T> = {}
15
+
16
+ function getValue(): T {
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
26
+ }
27
+ return cache.current
28
+ }
29
+
30
+ function setValue(value: SetValue<T>) {
31
+ if (cache.abortController) {
32
+ cache.abortController.abort()
33
+ }
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
39
+ }
40
+
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, {
55
+ onFinish() {
56
+ cache.current = getValue()
57
+ if (!canUpdate(cache, isEqual)) {
58
+ return
59
+ }
60
+ state.emitter.emit()
61
+ },
62
+ onResolveItem: setValue,
63
+ })
64
+
65
+ subscribeToDevelopmentTools(state)
66
+ return state
67
+ }