@codeleap/store 6.0.1 → 6.2.3
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/package.json +2 -2
- package/package.json.bak +1 -1
- package/src/globalState.ts +9 -9
- package/src/tests/globalState.spec.ts +207 -19
- package/src/types.ts +4 -1
- package/src/utils.ts +40 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeleap/store",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.3",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"directory": "packages/store"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@codeleap/config": "6.
|
|
12
|
+
"@codeleap/config": "6.2.3",
|
|
13
13
|
"ts-node-dev": "1.1.8"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
package/package.json.bak
CHANGED
package/src/globalState.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react'
|
|
2
2
|
import { setPersistentEngine, persistentAtom } from '@nanostores/persistent'
|
|
3
3
|
import { atom, WritableAtom } from 'nanostores'
|
|
4
|
-
import { GlobalState, GlobalStateConfig, StateSelector } from './types'
|
|
4
|
+
import { GlobalState, GlobalStateConfig, StateSelector, StateSetter } from './types'
|
|
5
5
|
import { stateAssign, useStateSelector } from './utils'
|
|
6
6
|
import { arrayHandler, arrayOps } from './array'
|
|
7
7
|
|
|
@@ -38,29 +38,29 @@ export function globalState<T>(value: T, config: GlobalStateConfig = defaultConf
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (prop === 'set') {
|
|
41
|
-
return (newValue: Partial<T
|
|
41
|
+
return (newValue: StateSetter<Partial<T>>) => {
|
|
42
42
|
const value = stateAssign(newValue, target.get())
|
|
43
43
|
target.set(value)
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
if(prop == 'reset'){
|
|
47
|
+
if (prop == 'reset') {
|
|
48
48
|
return Reflect.get(target, 'set', receiver)
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
if(arrayOps.includes(prop as string)){
|
|
50
|
+
|
|
51
|
+
if (arrayOps.includes(prop as string)) {
|
|
52
52
|
const currentValue = target.get()
|
|
53
|
-
|
|
54
|
-
if(!Array.isArray(currentValue)) {
|
|
53
|
+
|
|
54
|
+
if (!Array.isArray(currentValue)) {
|
|
55
55
|
throw new Error('Cannot call array methods on a non array store')
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const handle =
|
|
58
|
+
const handle = arrayHandler(target as WritableAtom<any[]>)
|
|
59
59
|
|
|
60
60
|
return Reflect.get(handle, prop, receiver)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
return Reflect.get(target, prop, receiver)
|
|
64
|
-
}
|
|
64
|
+
},
|
|
65
65
|
}) as unknown as GlobalState<T>
|
|
66
66
|
}
|
|
@@ -11,34 +11,115 @@ describe('globalState', () => {
|
|
|
11
11
|
expect(store.get()).toBe(4)
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
+
test('store.set with callback function', async () => {
|
|
15
|
+
const store = globalState(1)
|
|
16
|
+
|
|
17
|
+
store.set((x) => x + 1)
|
|
18
|
+
|
|
19
|
+
expect(store.get()).toBe(2)
|
|
20
|
+
|
|
21
|
+
const add = Array(10).fill(0).map(() => Math.round(Math.random() * 100))
|
|
22
|
+
|
|
23
|
+
const totalExpected = store.get() + add.reduce((acc, val) => acc + val)
|
|
24
|
+
|
|
25
|
+
add.forEach(async n => {
|
|
26
|
+
store.set(current => current + n)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(store.get()).toBe(totalExpected)
|
|
30
|
+
})
|
|
31
|
+
|
|
14
32
|
test('store.set() with object', () => {
|
|
15
33
|
const store = globalState({
|
|
16
34
|
a: 1,
|
|
17
|
-
b: 'Test'
|
|
35
|
+
b: 'Test',
|
|
18
36
|
})
|
|
19
37
|
|
|
20
38
|
store.set({
|
|
21
|
-
a: 4
|
|
39
|
+
a: 4,
|
|
22
40
|
})
|
|
23
41
|
|
|
24
42
|
expect(store.get().a).toBe(4)
|
|
25
43
|
})
|
|
44
|
+
|
|
45
|
+
test('store.set() with object preserves other keys', () => {
|
|
46
|
+
const store = globalState({
|
|
47
|
+
a: 1,
|
|
48
|
+
b: 'Test',
|
|
49
|
+
c: true,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
store.set({ a: 99 })
|
|
53
|
+
|
|
54
|
+
const val = store.get()
|
|
55
|
+
expect(val.a).toBe(99)
|
|
56
|
+
expect(val.b).toBe('Test')
|
|
57
|
+
expect(val.c).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('store.set() with callback on object', () => {
|
|
61
|
+
const store = globalState({
|
|
62
|
+
count: 0,
|
|
63
|
+
name: 'test',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
store.set((current) => ({ count: current.count + 5 }))
|
|
67
|
+
|
|
68
|
+
const val = store.get()
|
|
69
|
+
expect(val.count).toBe(5)
|
|
70
|
+
expect(val.name).toBe('test')
|
|
71
|
+
})
|
|
26
72
|
})
|
|
27
73
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
describe('reset method', () => {
|
|
75
|
+
test('store.reset() with object', () => {
|
|
76
|
+
const store = globalState({
|
|
77
|
+
a: 1,
|
|
78
|
+
b: 'Test',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
store.reset({
|
|
82
|
+
a: 4,
|
|
83
|
+
b: 'Changed',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const newVal = store.get()
|
|
87
|
+
expect(newVal.a).toBe(4)
|
|
88
|
+
expect(newVal.b).toBe('Changed')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('store.reset() with primitive', () => {
|
|
92
|
+
const store = globalState(100)
|
|
93
|
+
|
|
94
|
+
store.set(50)
|
|
95
|
+
expect(store.get()).toBe(50)
|
|
96
|
+
|
|
97
|
+
store.reset(100)
|
|
98
|
+
expect(store.get()).toBe(100)
|
|
32
99
|
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('get method', () => {
|
|
103
|
+
test('store.get() with selector', () => {
|
|
104
|
+
const store = globalState({
|
|
105
|
+
user: { name: 'John', age: 30 },
|
|
106
|
+
settings: { theme: 'dark' },
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const userName = store.get((s) => s.user.name)
|
|
110
|
+
const theme = store.get((s) => s.settings.theme)
|
|
33
111
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
b: 'Changed'
|
|
112
|
+
expect(userName).toBe('John')
|
|
113
|
+
expect(theme).toBe('dark')
|
|
37
114
|
})
|
|
38
115
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
116
|
+
test('store.get() without selector returns full state', () => {
|
|
117
|
+
const store = globalState({ a: 1, b: 2 })
|
|
118
|
+
|
|
119
|
+
const state = store.get()
|
|
120
|
+
|
|
121
|
+
expect(state).toEqual({ a: 1, b: 2 })
|
|
122
|
+
})
|
|
42
123
|
})
|
|
43
124
|
|
|
44
125
|
test('store array methods', () => {
|
|
@@ -58,16 +139,123 @@ describe('globalState', () => {
|
|
|
58
139
|
expect(doubled[1]).toBe(20)
|
|
59
140
|
})
|
|
60
141
|
|
|
61
|
-
|
|
62
|
-
|
|
142
|
+
describe('listen method', () => {
|
|
143
|
+
test('store.listen() receives current and previous values', () => {
|
|
144
|
+
const store = globalState(1)
|
|
145
|
+
|
|
146
|
+
store.listen((current, prev) => {
|
|
147
|
+
expect(current).toBe(4)
|
|
148
|
+
expect(prev).toBe(1)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
store.set(4)
|
|
152
|
+
|
|
153
|
+
expect(store.get()).toBe(4)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('store.listen() tracks multiple updates', () => {
|
|
157
|
+
const store = globalState(0)
|
|
158
|
+
const values: number[] = []
|
|
159
|
+
|
|
160
|
+
store.listen((current) => {
|
|
161
|
+
values.push(current)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
store.set(1)
|
|
165
|
+
store.set(2)
|
|
166
|
+
store.set(3)
|
|
167
|
+
|
|
168
|
+
expect(values).toEqual([1, 2, 3])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('store.listen() unsubscribe stops receiving updates', () => {
|
|
172
|
+
const store = globalState(0)
|
|
173
|
+
const values: number[] = []
|
|
174
|
+
|
|
175
|
+
const unsubscribe = store.listen((current) => {
|
|
176
|
+
values.push(current)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
store.set(1)
|
|
180
|
+
store.set(2)
|
|
181
|
+
|
|
182
|
+
unsubscribe()
|
|
183
|
+
|
|
184
|
+
store.set(3)
|
|
185
|
+
store.set(4)
|
|
186
|
+
|
|
187
|
+
expect(values).toEqual([1, 2])
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('array methods', () => {
|
|
192
|
+
test('mutating array methods update state', () => {
|
|
193
|
+
const store = globalState([] as number[])
|
|
63
194
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
195
|
+
store.push(10)
|
|
196
|
+
store.unshift(100)
|
|
197
|
+
|
|
198
|
+
const val = store.get()
|
|
199
|
+
|
|
200
|
+
expect(val[0]).toBe(100)
|
|
201
|
+
expect(val[1]).toBe(10)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('non-mutating array methods return correct values', () => {
|
|
205
|
+
const store = globalState([1, 2, 3, 4, 5])
|
|
206
|
+
|
|
207
|
+
const doubled = store.map((v) => v * 2)
|
|
208
|
+
const filtered = store.filter((v) => v > 2)
|
|
209
|
+
const found = store.find((v) => v === 3)
|
|
210
|
+
const index = store.indexOf(4)
|
|
211
|
+
|
|
212
|
+
expect(doubled).toEqual([2, 4, 6, 8, 10])
|
|
213
|
+
expect(filtered).toEqual([3, 4, 5])
|
|
214
|
+
expect(found).toBe(3)
|
|
215
|
+
expect(index).toBe(3)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('array methods on non-array store throws error', () => {
|
|
219
|
+
const store = globalState({ value: 1 })
|
|
220
|
+
|
|
221
|
+
expect(() => {
|
|
222
|
+
// @ts-expect-error - intentionally testing runtime error
|
|
223
|
+
store.push(10)
|
|
224
|
+
}).toThrow('Cannot call array methods on a non array store')
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('edge cases', () => {
|
|
229
|
+
test('store with null initial value', () => {
|
|
230
|
+
const store = globalState<string | null>(null)
|
|
231
|
+
|
|
232
|
+
expect(store.get()).toBe(null)
|
|
233
|
+
|
|
234
|
+
store.set('value')
|
|
235
|
+
expect(store.get()).toBe('value')
|
|
236
|
+
|
|
237
|
+
store.set(null)
|
|
238
|
+
expect(store.get()).toBe(null)
|
|
67
239
|
})
|
|
68
240
|
|
|
69
|
-
store
|
|
241
|
+
test('store with undefined initial value', () => {
|
|
242
|
+
const store = globalState<number | undefined>(undefined)
|
|
243
|
+
|
|
244
|
+
expect(store.get()).toBe(undefined)
|
|
70
245
|
|
|
71
|
-
|
|
246
|
+
store.reset(42)
|
|
247
|
+
expect(store.get()).toBe(42)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('store with empty object', () => {
|
|
251
|
+
const store = globalState<Record<string, number>>({})
|
|
252
|
+
|
|
253
|
+
store.set({ a: 1 })
|
|
254
|
+
expect(store.get()).toEqual({ a: 1 })
|
|
255
|
+
|
|
256
|
+
store.set({ b: 2 })
|
|
257
|
+
expect(store.get()).toEqual({ a: 1, b: 2 })
|
|
258
|
+
})
|
|
72
259
|
})
|
|
260
|
+
|
|
73
261
|
})
|
package/src/types.ts
CHANGED
|
@@ -3,10 +3,13 @@ import { PersistentStore, PersistentEvents, PersistentEvent } from '@nanostores/
|
|
|
3
3
|
|
|
4
4
|
export type StateSelector<S, R> = (state: S) => R
|
|
5
5
|
|
|
6
|
+
export type StateSetterFunction<In, Out = In> = (current: In) => Out
|
|
7
|
+
export type StateSetter<TIn, TOut = TIn> = TOut | StateSetterFunction<TIn, TOut>
|
|
8
|
+
|
|
6
9
|
export type GlobalState<T> = Omit<WritableAtom<T>, 'set' | 'get'> & {
|
|
7
10
|
use: <Selected = T>(selector?: StateSelector<T, Selected>) => Selected
|
|
8
11
|
|
|
9
|
-
set: (newValue: T extends Record<string, any> ? Partial<T
|
|
12
|
+
set: (newValue: T extends Record<string, any> ? StateSetter<T, Partial<T>> : StateSetter<T>) => void
|
|
10
13
|
|
|
11
14
|
get: <Selected = T>(selector?: StateSelector<T, Selected>) => Selected extends undefined ? T : Selected
|
|
12
15
|
|
package/src/utils.ts
CHANGED
|
@@ -1,47 +1,61 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react'
|
|
2
2
|
import { WritableAtom } from 'nanostores'
|
|
3
3
|
import { useMemo } from 'react'
|
|
4
|
+
import { StateSetter, StateSetterFunction } from './types'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
function isFunctionSetter<T>(x: any): x is StateSetterFunction<T> {
|
|
7
|
+
return typeof x === 'function'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveSetter<T>(setter: StateSetter<T>, currentValue:T):T {
|
|
11
|
+
if (isFunctionSetter(setter)) {
|
|
12
|
+
return setter(currentValue)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return setter
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stateAssign<T>(newValue: StateSetter<Partial<T>>, stateValue: T): T {
|
|
19
|
+
const resolvedValue = resolveSetter(newValue, stateValue)
|
|
6
20
|
if (
|
|
7
|
-
typeof stateValue ===
|
|
21
|
+
typeof stateValue === 'object' && stateValue !== null
|
|
8
22
|
) {
|
|
9
23
|
return {
|
|
10
24
|
...stateValue,
|
|
11
|
-
...
|
|
25
|
+
...resolvedValue,
|
|
12
26
|
} as T
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return resolvedValue as T
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
export const createStateSlice = <T, R, S extends WritableAtom<T>>(
|
|
19
33
|
store: S,
|
|
20
34
|
selector: (state: T) => R,
|
|
21
|
-
deselector?: (result: R) => Partial<T
|
|
35
|
+
deselector?: (result: R) => Partial<T>,
|
|
22
36
|
) => ({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} as WritableAtom<R>)
|
|
37
|
+
get: () => selector(store.get()),
|
|
38
|
+
listen: (listener: (value: R) => void) => {
|
|
39
|
+
return store.listen((state) => {
|
|
40
|
+
listener(selector(state))
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
set(v: R) {
|
|
44
|
+
if (!deselector) {
|
|
45
|
+
throw new Error('[createStateSelector] deselector must be implemented to call set on state slices')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsed = deselector(v)
|
|
49
|
+
|
|
50
|
+
const newValue = stateAssign(parsed, store.get())
|
|
51
|
+
|
|
52
|
+
store.set(newValue)
|
|
53
|
+
},
|
|
54
|
+
} as WritableAtom<R>)
|
|
41
55
|
|
|
42
56
|
export function useStateSelector<T, R, S extends WritableAtom<T>>(
|
|
43
57
|
store: S,
|
|
44
|
-
selector: (state: T) => R
|
|
58
|
+
selector: (state: T) => R,
|
|
45
59
|
): R {
|
|
46
60
|
const slice = useMemo(() => createStateSlice(store, selector), [selector])
|
|
47
61
|
|