@codeleap/store 6.1.2 → 6.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeleap/store",
3
- "version": "6.1.2",
3
+ "version": "6.3.0",
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.1.2",
12
+ "@codeleap/config": "6.3.0",
13
13
  "ts-node-dev": "1.1.8"
14
14
  },
15
15
  "scripts": {
package/package.json.bak CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeleap/store",
3
- "version": "6.1.2",
3
+ "version": "6.3.0",
4
4
  "main": "src/index.ts",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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 = arrayHandler(target as WritableAtom<any[]>)
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
- test('store.reset() with object', () => {
29
- const store = globalState({
30
- a: 1,
31
- b: 'Test'
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
- store.reset({
35
- a: 4,
36
- b: 'Changed'
112
+ expect(userName).toBe('John')
113
+ expect(theme).toBe('dark')
37
114
  })
38
115
 
39
- const newVal = store.get()
40
- expect(newVal.a).toBe(4)
41
- expect(newVal.b).toBe('Changed')
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
- test('store.listen()', () => {
62
- const store = globalState(1)
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
- store.listen((current, prev) => {
65
- expect(current).toBe(4)
66
- expect(prev).toBe(1)
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.set(4)
241
+ test('store with undefined initial value', () => {
242
+ const store = globalState<number | undefined>(undefined)
243
+
244
+ expect(store.get()).toBe(undefined)
70
245
 
71
- expect(store.get()).toBe(4)
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> : T) => void
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
- export function stateAssign<T>(newValue: Partial<T>, stateValue: T): T {
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 === "object" && stateValue !== null
21
+ typeof stateValue === 'object' && stateValue !== null
8
22
  ) {
9
23
  return {
10
24
  ...stateValue,
11
- ...newValue,
25
+ ...resolvedValue,
12
26
  } as T
13
- }
14
-
15
- return newValue as T
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
- get: () => selector(store.get()),
24
- listen: (listener: (value: R) => void) => {
25
- return store.listen((state) => {
26
- listener(selector(state))
27
- })
28
- },
29
- set(v: R) {
30
- if(!deselector) {
31
- throw new Error('[createStateSelector] deselector must be implemented to call set on state slices')
32
- }
33
-
34
- const parsed = deselector(v)
35
-
36
- const newValue = stateAssign(parsed, store.get())
37
-
38
- store.set(newValue)
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