@codeleap/store
Advanced tools
+2
-2
| { | ||
| "name": "@codeleap/store", | ||
| "version": "6.1.2", | ||
| "version": "6.2.3", | ||
| "main": "src/index.ts", | ||
@@ -12,3 +12,3 @@ "license": "UNLICENSED", | ||
| "devDependencies": { | ||
| "@codeleap/config": "6.1.2", | ||
| "@codeleap/config": "6.2.3", | ||
| "ts-node-dev": "1.1.8" | ||
@@ -15,0 +15,0 @@ }, |
| import { useStore } from '@nanostores/react' | ||
| import { setPersistentEngine, persistentAtom } from '@nanostores/persistent' | ||
| import { atom, WritableAtom } from 'nanostores' | ||
| import { GlobalState, GlobalStateConfig, StateSelector } from './types' | ||
| import { GlobalState, GlobalStateConfig, StateSelector, StateSetter } from './types' | ||
| import { stateAssign, useStateSelector } from './utils' | ||
@@ -41,3 +41,3 @@ import { arrayHandler, arrayOps } from './array' | ||
| if (prop === 'set') { | ||
| return (newValue: Partial<T>) => { | ||
| return (newValue: StateSetter<Partial<T>>) => { | ||
| const value = stateAssign(newValue, target.get()) | ||
@@ -48,14 +48,14 @@ target.set(value) | ||
| if(prop == 'reset'){ | ||
| if (prop == 'reset') { | ||
| return Reflect.get(target, 'set', receiver) | ||
| } | ||
| if(arrayOps.includes(prop as string)){ | ||
| if (arrayOps.includes(prop as string)) { | ||
| const currentValue = target.get() | ||
| if(!Array.isArray(currentValue)) { | ||
| if (!Array.isArray(currentValue)) { | ||
| throw new Error('Cannot call array methods on a non array store') | ||
| } | ||
| const handle = arrayHandler(target as WritableAtom<any[]>) | ||
| const handle = arrayHandler(target as WritableAtom<any[]>) | ||
@@ -66,4 +66,4 @@ return Reflect.get(handle, prop, receiver) | ||
| return Reflect.get(target, prop, receiver) | ||
| } | ||
| }, | ||
| }) as unknown as GlobalState<T> | ||
| } |
@@ -14,10 +14,28 @@ import { expect, test, describe } from 'bun:test' | ||
| test('store.set with callback function', async () => { | ||
| const store = globalState(1) | ||
| store.set((x) => x + 1) | ||
| expect(store.get()).toBe(2) | ||
| const add = Array(10).fill(0).map(() => Math.round(Math.random() * 100)) | ||
| const totalExpected = store.get() + add.reduce((acc, val) => acc + val) | ||
| add.forEach(async n => { | ||
| store.set(current => current + n) | ||
| }) | ||
| expect(store.get()).toBe(totalExpected) | ||
| }) | ||
| test('store.set() with object', () => { | ||
| const store = globalState({ | ||
| a: 1, | ||
| b: 'Test' | ||
| b: 'Test', | ||
| }) | ||
| store.set({ | ||
| a: 4 | ||
| a: 4, | ||
| }) | ||
@@ -27,18 +45,81 @@ | ||
| }) | ||
| test('store.set() with object preserves other keys', () => { | ||
| const store = globalState({ | ||
| a: 1, | ||
| b: 'Test', | ||
| c: true, | ||
| }) | ||
| store.set({ a: 99 }) | ||
| const val = store.get() | ||
| expect(val.a).toBe(99) | ||
| expect(val.b).toBe('Test') | ||
| expect(val.c).toBe(true) | ||
| }) | ||
| test('store.set() with callback on object', () => { | ||
| const store = globalState({ | ||
| count: 0, | ||
| name: 'test', | ||
| }) | ||
| store.set((current) => ({ count: current.count + 5 })) | ||
| const val = store.get() | ||
| expect(val.count).toBe(5) | ||
| expect(val.name).toBe('test') | ||
| }) | ||
| }) | ||
| test('store.reset() with object', () => { | ||
| const store = globalState({ | ||
| a: 1, | ||
| b: 'Test' | ||
| describe('reset method', () => { | ||
| test('store.reset() with object', () => { | ||
| const store = globalState({ | ||
| a: 1, | ||
| b: 'Test', | ||
| }) | ||
| store.reset({ | ||
| a: 4, | ||
| b: 'Changed', | ||
| }) | ||
| const newVal = store.get() | ||
| expect(newVal.a).toBe(4) | ||
| expect(newVal.b).toBe('Changed') | ||
| }) | ||
| store.reset({ | ||
| a: 4, | ||
| b: 'Changed' | ||
| test('store.reset() with primitive', () => { | ||
| const store = globalState(100) | ||
| store.set(50) | ||
| expect(store.get()).toBe(50) | ||
| store.reset(100) | ||
| expect(store.get()).toBe(100) | ||
| }) | ||
| }) | ||
| const newVal = store.get() | ||
| expect(newVal.a).toBe(4) | ||
| expect(newVal.b).toBe('Changed') | ||
| describe('get method', () => { | ||
| test('store.get() with selector', () => { | ||
| const store = globalState({ | ||
| user: { name: 'John', age: 30 }, | ||
| settings: { theme: 'dark' }, | ||
| }) | ||
| const userName = store.get((s) => s.user.name) | ||
| const theme = store.get((s) => s.settings.theme) | ||
| expect(userName).toBe('John') | ||
| expect(theme).toBe('dark') | ||
| }) | ||
| test('store.get() without selector returns full state', () => { | ||
| const store = globalState({ a: 1, b: 2 }) | ||
| const state = store.get() | ||
| expect(state).toEqual({ a: 1, b: 2 }) | ||
| }) | ||
| }) | ||
@@ -63,14 +144,121 @@ | ||
| test('store.listen()', () => { | ||
| const store = globalState(1) | ||
| describe('listen method', () => { | ||
| test('store.listen() receives current and previous values', () => { | ||
| const store = globalState(1) | ||
| store.listen((current, prev) => { | ||
| expect(current).toBe(4) | ||
| expect(prev).toBe(1) | ||
| store.listen((current, prev) => { | ||
| expect(current).toBe(4) | ||
| expect(prev).toBe(1) | ||
| }) | ||
| store.set(4) | ||
| expect(store.get()).toBe(4) | ||
| }) | ||
| store.set(4) | ||
| test('store.listen() tracks multiple updates', () => { | ||
| const store = globalState(0) | ||
| const values: number[] = [] | ||
| expect(store.get()).toBe(4) | ||
| store.listen((current) => { | ||
| values.push(current) | ||
| }) | ||
| store.set(1) | ||
| store.set(2) | ||
| store.set(3) | ||
| expect(values).toEqual([1, 2, 3]) | ||
| }) | ||
| test('store.listen() unsubscribe stops receiving updates', () => { | ||
| const store = globalState(0) | ||
| const values: number[] = [] | ||
| const unsubscribe = store.listen((current) => { | ||
| values.push(current) | ||
| }) | ||
| store.set(1) | ||
| store.set(2) | ||
| unsubscribe() | ||
| store.set(3) | ||
| store.set(4) | ||
| expect(values).toEqual([1, 2]) | ||
| }) | ||
| }) | ||
| describe('array methods', () => { | ||
| test('mutating array methods update state', () => { | ||
| const store = globalState([] as number[]) | ||
| store.push(10) | ||
| store.unshift(100) | ||
| const val = store.get() | ||
| expect(val[0]).toBe(100) | ||
| expect(val[1]).toBe(10) | ||
| }) | ||
| test('non-mutating array methods return correct values', () => { | ||
| const store = globalState([1, 2, 3, 4, 5]) | ||
| const doubled = store.map((v) => v * 2) | ||
| const filtered = store.filter((v) => v > 2) | ||
| const found = store.find((v) => v === 3) | ||
| const index = store.indexOf(4) | ||
| expect(doubled).toEqual([2, 4, 6, 8, 10]) | ||
| expect(filtered).toEqual([3, 4, 5]) | ||
| expect(found).toBe(3) | ||
| expect(index).toBe(3) | ||
| }) | ||
| test('array methods on non-array store throws error', () => { | ||
| const store = globalState({ value: 1 }) | ||
| expect(() => { | ||
| // @ts-expect-error - intentionally testing runtime error | ||
| store.push(10) | ||
| }).toThrow('Cannot call array methods on a non array store') | ||
| }) | ||
| }) | ||
| describe('edge cases', () => { | ||
| test('store with null initial value', () => { | ||
| const store = globalState<string | null>(null) | ||
| expect(store.get()).toBe(null) | ||
| store.set('value') | ||
| expect(store.get()).toBe('value') | ||
| store.set(null) | ||
| expect(store.get()).toBe(null) | ||
| }) | ||
| test('store with undefined initial value', () => { | ||
| const store = globalState<number | undefined>(undefined) | ||
| expect(store.get()).toBe(undefined) | ||
| store.reset(42) | ||
| expect(store.get()).toBe(42) | ||
| }) | ||
| test('store with empty object', () => { | ||
| const store = globalState<Record<string, number>>({}) | ||
| store.set({ a: 1 }) | ||
| expect(store.get()).toEqual({ a: 1 }) | ||
| store.set({ b: 2 }) | ||
| expect(store.get()).toEqual({ a: 1, b: 2 }) | ||
| }) | ||
| }) | ||
| }) |
+4
-1
@@ -6,6 +6,9 @@ import { WritableAtom } from 'nanostores' | ||
| export type StateSetterFunction<In, Out = In> = (current: In) => Out | ||
| export type StateSetter<TIn, TOut = TIn> = TOut | StateSetterFunction<TIn, TOut> | ||
| export type GlobalState<T> = Omit<WritableAtom<T>, 'set' | 'get'> & { | ||
| use: <Selected = T>(selector?: StateSelector<T, Selected>) => Selected | ||
| set: (newValue: T extends Record<string, any> ? Partial<T> : T) => void | ||
| set: (newValue: T extends Record<string, any> ? StateSetter<T, Partial<T>> : StateSetter<T>) => void | ||
@@ -12,0 +15,0 @@ get: <Selected = T>(selector?: StateSelector<T, Selected>) => Selected extends undefined ? T : Selected |
+38
-24
| import { useStore } from '@nanostores/react' | ||
| import { WritableAtom } from 'nanostores' | ||
| import { useMemo } from 'react' | ||
| import { StateSetter, StateSetterFunction } from './types' | ||
| export function stateAssign<T>(newValue: Partial<T>, stateValue: T): T { | ||
| function isFunctionSetter<T>(x: any): x is StateSetterFunction<T> { | ||
| return typeof x === 'function' | ||
| } | ||
| function resolveSetter<T>(setter: StateSetter<T>, currentValue:T):T { | ||
| if (isFunctionSetter(setter)) { | ||
| return setter(currentValue) | ||
| } | ||
| return setter | ||
| } | ||
| export function stateAssign<T>(newValue: StateSetter<Partial<T>>, stateValue: T): T { | ||
| const resolvedValue = resolveSetter(newValue, stateValue) | ||
| if ( | ||
| typeof stateValue === "object" && stateValue !== null | ||
| typeof stateValue === 'object' && stateValue !== null | ||
| ) { | ||
| return { | ||
| ...stateValue, | ||
| ...newValue, | ||
| ...resolvedValue, | ||
| } as T | ||
| } | ||
| return newValue as T | ||
| } | ||
| return resolvedValue as T | ||
| } | ||
@@ -21,26 +35,26 @@ | ||
| selector: (state: T) => R, | ||
| deselector?: (result: R) => Partial<T> | ||
| deselector?: (result: R) => Partial<T>, | ||
| ) => ({ | ||
| get: () => selector(store.get()), | ||
| listen: (listener: (value: R) => void) => { | ||
| return store.listen((state) => { | ||
| listener(selector(state)) | ||
| }) | ||
| }, | ||
| set(v: R) { | ||
| if(!deselector) { | ||
| throw new Error('[createStateSelector] deselector must be implemented to call set on state slices') | ||
| } | ||
| get: () => selector(store.get()), | ||
| listen: (listener: (value: R) => void) => { | ||
| return store.listen((state) => { | ||
| listener(selector(state)) | ||
| }) | ||
| }, | ||
| set(v: R) { | ||
| if (!deselector) { | ||
| throw new Error('[createStateSelector] deselector must be implemented to call set on state slices') | ||
| } | ||
| const parsed = deselector(v) | ||
| const newValue = stateAssign(parsed, store.get()) | ||
| const parsed = deselector(v) | ||
| store.set(newValue) | ||
| } | ||
| } as WritableAtom<R>) | ||
| const newValue = stateAssign(parsed, store.get()) | ||
| store.set(newValue) | ||
| }, | ||
| } as WritableAtom<R>) | ||
| export function useStateSelector<T, R, S extends WritableAtom<T>>( | ||
| store: S, | ||
| selector: (state: T) => R | ||
| selector: (state: T) => R, | ||
| ): R { | ||
@@ -47,0 +61,0 @@ const slice = useMemo(() => createStateSlice(store, selector), [selector]) |
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
13158
65.59%371
67.12%0
-100%