This commit is contained in:
2025-09-30 12:21:27 +02:00
parent 07e54ddeab
commit 2b7e09d9c9
3842 changed files with 283556 additions and 109 deletions

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright 2020 Andrey Sitnik <andrey@sitnik.ru>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,63 @@
# Nano Stores
<img align="right" width="92" height="92" title="Nano Stores logo"
src="https://nanostores.github.io/nanostores/logo.svg">
A tiny state manager for **React**, **React Native**, **Preact**, **Vue**,
**Svelte**, **Solid**, **Lit**, **Angular**, and vanilla JS.
It uses **many atomic stores** and direct manipulation.
* **Small.** Between 265 and 803 bytes (minified and brotlied).
Zero dependencies. It uses [Size Limit] to control size.
* **Fast.** With small atomic and derived stores, you do not need to call
the selector function for all components on every store change.
* **Tree Shakable.** A chunk contains only stores used by components
in the chunk.
* Designed to move logic from components to stores.
* Good **TypeScript** support.
```ts
// store/users.ts
import { atom } from 'nanostores'
export const $users = atom<User[]>([])
export function addUser(user: User) {
$users.set([...$users.get(), user]);
}
```
```ts
// store/admins.ts
import { computed } from 'nanostores'
import { $users } from './users.js'
export const $admins = computed($users, users => users.filter(i => i.isAdmin))
```
```tsx
// components/admins.tsx
import { useStore } from '@nanostores/react'
import { $admins } from '../stores/admins.js'
export const Admins = () => {
const admins = useStore($admins)
return (
<ul>
{admins.map(user => <UserItem user={user} />)}
</ul>
)
}
```
---
<img src="https://cdn.evilmartians.com/badges/logo-no-label.svg" alt="" width="22" height="16" />  Made at <b><a href="https://evilmartians.com/devtools?utm_source=nanostores&utm_campaign=devtools-button&utm_medium=github">Evil Martians</a></b>, product consulting for <b>developer tools</b>.
---
[Size Limit]: https://github.com/ai/size-limit
## Docs
Read full docs **[here](https://github.com/nanostores/nanostores#readme)**.

View File

@ -0,0 +1,169 @@
export type AllKeys<T> = T extends any ? keyof T : never
type Primitive = boolean | number | string
export type ReadonlyIfObject<Value> = Value extends undefined
? Value
: Value extends (...args: any) => any
? Value
: Value extends Primitive
? Value
: Value extends object
? Readonly<Value>
: Value
/**
* Store object.
*/
export interface ReadableAtom<Value = any> {
/**
* Get store value.
*
* In contrast with {@link ReadableAtom#value} this value will be always
* initialized even if store had no listeners.
*
* ```js
* $store.get()
* ```
*
* @returns Store value.
*/
get(): Value
/**
* Listeners count.
*/
readonly lc: number
/**
* Subscribe to store changes.
*
* In contrast with {@link Store#subscribe} it do not call listener
* immediately.
*
* @param listener Callback with store value and old value.
* @returns Function to remove listener.
*/
listen(
listener: (
value: ReadonlyIfObject<Value>,
oldValue: ReadonlyIfObject<Value>
) => void
): () => void
/**
* Low-level method to notify listeners about changes in the store.
*
* Can cause unexpected behaviour when combined with frontend frameworks
* that perform equality checks for values, such as React.
*/
notify(oldValue?: ReadonlyIfObject<Value>): void
/**
* Unbind all listeners.
*/
off(): void
/**
* Subscribe to store changes and call listener immediately.
*
* ```
* import { $router } from '../store'
*
* $router.subscribe(page => {
* console.log(page)
* })
* ```
*
* @param listener Callback with store value and old value.
* @returns Function to remove listener.
*/
subscribe(
listener: (
value: ReadonlyIfObject<Value>,
oldValue?: ReadonlyIfObject<Value>
) => void
): () => void
/**
* Low-level method to read stores value without calling `onStart`.
*
* Try to use only {@link ReadableAtom#get}.
* Without subscribers, value can be undefined.
*/
readonly value: undefined | Value
}
/**
* Store with a way to manually change the value.
*/
export interface WritableAtom<Value = any> extends ReadableAtom<Value> {
/**
* Change store value.
*
* ```js
* $router.set({ path: location.pathname, page: parse(location.pathname) })
* ```
*
* @param newValue New store value.
*/
set(newValue: Value): void
}
export interface PreinitializedWritableAtom<Value> extends WritableAtom<Value> {
readonly value: Value
}
export type Atom<Value = any> = ReadableAtom<Value> | WritableAtom<Value>
export declare let notifyId: number
/**
* Create store with atomic value. It could be a string or an object, which you
* will replace completely.
*
* If you want to change keys in the object inside store, use {@link map}.
*
* ```js
* import { atom, onMount } from 'nanostores'
*
* // Initial value
* export const $router = atom({ path: '', page: 'home' })
*
* function parse () {
* $router.set({ path: location.pathname, page: parse(location.pathname) })
* }
*
* // Listen for URL changes on first stores listener.
* onMount($router, () => {
* parse()
* window.addEventListener('popstate', parse)
* return () => {
* window.removeEventListener('popstate', parse)
* }
* })
* ```
*
* @param initialValue Initial value of the store.
* @returns The store object with methods to subscribe.
*/
export function atom<Value, StoreExt = object>(
...args: undefined extends Value ? [] | [Value] : [Value]
): PreinitializedWritableAtom<Value> & StoreExt
/**
* Change store type for readonly for export.
*
* ```ts
* import { readonlyType } from 'nanostores'
*
* const $storePrivate = atom(0)
*
* export const $store = readonlyType($storePrivate)
* ```
*
* @param store The store to be exported.
* @returns The readonly store.
*/
export function readonlyType<Value>(
store: ReadableAtom<Value>
): ReadableAtom<Value>

View File

@ -0,0 +1,92 @@
import { clean } from '../clean-stores/index.js'
let listenerQueue = []
let lqIndex = 0
const QUEUE_ITEMS_PER_LISTENER = 4
export let epoch = 0
export let atom = initialValue => {
let listeners = []
let $atom = {
get() {
if (!$atom.lc) {
$atom.listen(() => {})()
}
return $atom.value
},
lc: 0,
listen(listener) {
$atom.lc = listeners.push(listener)
return () => {
for (
let i = lqIndex + QUEUE_ITEMS_PER_LISTENER;
i < listenerQueue.length;
) {
if (listenerQueue[i] === listener) {
listenerQueue.splice(i, QUEUE_ITEMS_PER_LISTENER)
} else {
i += QUEUE_ITEMS_PER_LISTENER
}
}
let index = listeners.indexOf(listener)
if (~index) {
listeners.splice(index, 1)
if (!--$atom.lc) $atom.off()
}
}
},
notify(oldValue, changedKey) {
epoch++
let runListenerQueue = !listenerQueue.length
for (let listener of listeners) {
listenerQueue.push(listener, $atom.value, oldValue, changedKey)
}
if (runListenerQueue) {
for (
lqIndex = 0;
lqIndex < listenerQueue.length;
lqIndex += QUEUE_ITEMS_PER_LISTENER
) {
listenerQueue[lqIndex](
listenerQueue[lqIndex + 1],
listenerQueue[lqIndex + 2],
listenerQueue[lqIndex + 3]
)
}
listenerQueue.length = 0
}
},
/* It will be called on last listener unsubscribing.
We will redefine it in onMount and onStop. */
off() {},
set(newValue) {
let oldValue = $atom.value
if (oldValue !== newValue) {
$atom.value = newValue
$atom.notify(oldValue)
}
},
subscribe(listener) {
let unbind = $atom.listen(listener)
listener($atom.value)
return unbind
},
value: initialValue
}
if (process.env.NODE_ENV !== 'production') {
$atom[clean] = () => {
listeners = []
$atom.lc = 0
$atom.off()
}
}
return $atom
}
export const readonlyType = store => store

View File

@ -0,0 +1,24 @@
import type { MapCreator } from '../map-creator/index.js'
import type { Store } from '../map/index.js'
export const clean: unique symbol
/**
* Destroys all cached stores and call
*
* It also reset all tasks by calling {@link cleanTasks}.
*
* ```js
* import { cleanStores } from 'nanostores'
*
* afterEach(() => {
* cleanStores($router, $settings)
* })
* ```
*
* @param stores Used store classes.
* @return Promise for stores destroying.
*/
export function cleanStores(
...stores: (MapCreator<any, any[]> | Store | undefined)[]
): void

View File

@ -0,0 +1,18 @@
import { cleanTasks } from '../task/index.js'
export let clean = Symbol('clean')
export let cleanStores = (...stores) => {
if (process.env.NODE_ENV === 'production') {
throw new Error(
'cleanStores() can be used only during development or tests'
)
}
cleanTasks()
for (let $store of stores) {
if ($store) {
if ($store.mocked) delete $store.mocked
if ($store[clean]) $store[clean]()
}
}
}

View File

@ -0,0 +1,86 @@
import type { ReadableAtom } from '../atom/index.js'
import type { AnyStore, Store, StoreValue } from '../map/index.js'
import type { Task } from '../task/index.js'
export type StoreValues<Stores extends AnyStore[]> = {
[Index in keyof Stores]: StoreValue<Stores[Index]>
}
type A = ReadableAtom<number>
type B = ReadableAtom<string>
type C = (...values: StoreValues<[A, B]>) => void
interface Computed {
<Value, OriginStore extends Store>(
stores: OriginStore,
cb: (value: StoreValue<OriginStore>) => Task<Value>
): ReadableAtom<undefined | Value>
<Value, OriginStores extends AnyStore[]>(
stores: [...OriginStores],
cb: (...values: StoreValues<OriginStores>) => Task<Value>
): ReadableAtom<undefined | Value>
<Value, OriginStore extends Store>(
stores: OriginStore,
cb: (value: StoreValue<OriginStore>) => Value
): ReadableAtom<Value>
/**
* Create derived store, which use generates value from another stores.
*
* ```js
* import { computed } from 'nanostores'
*
* import { $users } from './users.js'
*
* export const $admins = computed($users, users => {
* return users.filter(user => user.isAdmin)
* })
* ```
*
* An async function can be evaluated by using {@link task}.
*
* ```js
* import { computed, task } from 'nanostores'
*
* import { $userId } from './users.js'
*
* export const $user = computed($userId, userId => task(async () => {
* const response = await fetch(`https://my-api/users/${userId}`)
* return response.json()
* }))
* ```
*/
<Value, OriginStores extends AnyStore[]>(
stores: [...OriginStores],
cb: (...values: StoreValues<OriginStores>) => Task<Value> | Value
): ReadableAtom<Value>
}
export const computed: Computed
interface Batched {
<Value, OriginStore extends Store>(
stores: OriginStore,
cb: (value: StoreValue<OriginStore>) => Task<Value> | Value
): ReadableAtom<Value>
/**
* Create derived store, which use generates value from another stores.
*
* ```js
* import { batched } from 'nanostores'
*
* const $sortBy = atom('id')
* const $category = atom('')
*
* export const $link = batched([$sortBy, $category], (sortBy, category) => {
* return `/api/entities?sortBy=${sortBy}&category=${category}`
* })
* ```
*/
<Value, OriginStores extends AnyStore[]>(
stores: [...OriginStores],
cb: (...values: StoreValues<OriginStores>) => Task<Value> | Value
): ReadableAtom<Value>
}
export const batched: Batched

View File

@ -0,0 +1,56 @@
import { atom, epoch } from '../atom/index.js'
import { onMount } from '../lifecycle/index.js'
let computedStore = (stores, cb, batched) => {
if (!Array.isArray(stores)) stores = [stores]
let previousArgs
let currentEpoch
let set = () => {
if (currentEpoch === epoch) return
currentEpoch = epoch
let args = stores.map($store => $store.get())
if (!previousArgs || args.some((arg, i) => arg !== previousArgs[i])) {
previousArgs = args
let value = cb(...args)
if (value && value.then && value.t) {
value.then(asyncValue => {
if (previousArgs === args) {
// Prevent a stale set
$computed.set(asyncValue)
}
})
} else {
$computed.set(value)
currentEpoch = epoch
}
}
}
let $computed = atom(undefined)
let get = $computed.get
$computed.get = () => {
set()
return get()
}
let timer
let run = batched
? () => {
clearTimeout(timer)
timer = setTimeout(set)
}
: set
onMount($computed, () => {
let unbinds = stores.map($store => $store.listen(run))
set()
return () => {
for (let unbind of unbinds) unbind()
}
})
return $computed
}
export let computed = (stores, fn) => computedStore(stores, fn)
export let batched = (stores, fn) => computedStore(stores, fn, true)

View File

@ -0,0 +1,127 @@
import type { WritableAtom } from '../atom/index.js'
import type { AnyStore } from '../map/index.js'
import type {
AllPaths,
BaseDeepMap,
FromPath,
FromPathWithIndexSignatureUndefined
} from './path.js'
export {
AllPaths,
BaseDeepMap,
FromPath,
getPath,
setByKey,
setPath
} from './path.js'
export type DeepMapStore<T extends BaseDeepMap> = {
/**
* Subscribe to store changes.
*
* In contrast with {@link Store#subscribe} it do not call listener
* immediately.
*
* @param listener Callback with store value and old value.
* @param changedKey Key that was changed. Will present only if `setKey`
* has been used to change a store.
* @returns Function to remove listener.
*/
listen(
listener: (
value: T,
oldValue: T,
changedKey: AllPaths<T> | undefined
) => void
): () => void
/**
* Low-level method to notify listeners about changes in the store.
*
* Can cause unexpected behaviour when combined with frontend frameworks
* doing equality checks for values, e.g. React.
*/
notify(oldValue?: T, changedKey?: AllPaths<T>): void
/**
* Change key in store value. Copies are made at each level of `key` so that
* no part of the original object is mutated (but it does not do a full deep
* copy -- some sub-objects may still be shared between the old value and the
* new one).
*
* ```js
* $settings.setKey('visuals.theme', 'dark')
* ```
*
* @param key The key name. Attributes can be split with a dot `.` and `[]`.
* @param value New value.
*/
setKey: <K extends AllPaths<T>>(
key: K,
value: FromPathWithIndexSignatureUndefined<T, K>
) => void
/**
* Subscribe to store changes and call listener immediately.
*
* ```
* import { $settings } from '../store'
*
* $settings.subscribe(settings => {
* console.log(settings)
* })
* ```
*
* @param listener Callback with store value and old value.
* @param changedKey Key that was changed. Will present only
* if `setKey` has been used to change a store.
* @returns Function to remove listener.
*/
subscribe(
listener: (
value: T,
oldValue: T | undefined,
changedKey: AllPaths<T> | undefined
) => void
): () => void
} & Omit<WritableAtom<T>, 'listen' | 'notify' | 'setKey' | 'subscribe'>
/**
* Create deep map store. Deep map store is a store with an object as store
* value, that supports fine-grained reactivity for deeply nested properties.
*
* @param init Initialize store and return store destructor.
* @returns The store object with methods to subscribe.
*/
export function deepMap<T extends BaseDeepMap>(init?: T): DeepMapStore<T>
/**
* Get a value by key from a store with an object value.
* Works with `map`, `deepMap`, and `atom`.
*
* ```js
* import { getKey, map } from 'nanostores'
*
* const $user = map({ name: 'John', profile: { age: 30 } })
*
* // Simple key access
* getKey($user, 'name') // Returns 'John'
*
* // Nested access with dot notation
* getKey($user, 'profile.age') // Returns 30
*
* // Array access
* const $items = map({ products: ['apple', 'banana'] })
* getKey($items, 'products[1]') // Returns 'banana'
* ```
*
* @param store The store to get the value from.
* @param key The key to access. Can be a simple key or a path with dot notation.
* @returns The value for this key
*/
export function getKey<
T extends Record<string, unknown>,
K extends AllPaths<T>
>(store: AnyStore<T>, key: K): FromPath<T, K>

View File

@ -0,0 +1,21 @@
import { atom } from '../atom/index.js'
import { getPath, setPath } from './path.js'
export { getPath, setByKey, setPath } from './path.js'
export function deepMap(initial = {}) {
let $deepMap = atom(initial)
$deepMap.setKey = (key, value) => {
if (getPath($deepMap.value, key) !== value) {
let oldValue = $deepMap.value
$deepMap.value = setPath($deepMap.value, key, value)
$deepMap.notify(oldValue, key)
}
}
return $deepMap
}
export function getKey(store, key) {
let value = store.get()
return getPath(value, key)
}

View File

@ -0,0 +1,169 @@
import type { ValueWithUndefinedForIndexSignatures } from '../map/index.js'
type ConcatPath<T extends string, P extends string> = T extends ''
? P
: `${T}.${P}`
type Length<T extends any[]> = T extends { length: infer L } ? L : never
type BuildTuple<L extends number, T extends any[] = []> = T extends {
length: L
}
? T
: BuildTuple<L, [...T, any]>
type Subtract<A extends number, B extends number> = BuildTuple<A> extends [
...infer U,
...BuildTuple<B>
]
? Length<U>
: never
export type AllPaths<
T,
P extends string = '',
MaxDepth extends number = 10
> = T extends (infer U)[]
?
| `${P}[${number}]`
| AllPaths<U, `${P}[${number}]`, Subtract<MaxDepth, 1>>
| P
: T extends BaseDeepMap
? MaxDepth extends 0
? never
: {
[K in keyof T]-?: K extends number | string
?
| AllPaths<T[K], ConcatPath<P, `${K}`>, Subtract<MaxDepth, 1>>
| (P extends '' ? never : P)
: never
}[keyof T]
: P
type IsNumber<T extends string> = T extends `${number}` ? true : false
type ElementType<T> = T extends (infer U)[] ? U : never
type Unwrap<T, P> = P extends `[${infer I}]${infer R}`
? [ElementType<T>, IsNumber<I>] extends [infer Item, true]
? R extends ''
? Item
: Unwrap<Item, R>
: never
: never
type NestedObjKey<T, P> = P extends `${infer A}.${infer B}`
? A extends keyof T
? FromPath<NonNullable<T[A]>, B>
: never
: never
type NestedObjKeyWithIndexSignatureUndefined<T, P> =
P extends `${infer A}.${infer B}`
? A extends keyof T
? FromPathWithIndexSignatureUndefined<NonNullable<T[A]>, B>
: never
: never
type NestedArrKey<T, P> = P extends `${infer A}[${infer I}]${infer R}`
? [A, NonNullable<T[Extract<A, keyof T>]>, IsNumber<I>] extends [
keyof T,
(infer Item)[],
true
]
? R extends ''
? Item
: R extends `.${infer NewR}`
? FromPath<Item, NewR>
: R extends `${infer Indices}.${infer MoreR}`
? FromPath<Unwrap<Item, Indices>, MoreR>
: Unwrap<Item, R>
: never
: never
export type FromPath<T, P> = T extends unknown
? NestedArrKey<T, P> extends never
? NestedObjKey<T, P> extends never
? P extends keyof T
? T[P]
: never
: NestedObjKey<T, P>
: NestedArrKey<T, P>
: never
export type FromPathWithIndexSignatureUndefined<T, P> = T extends unknown
? NestedArrKey<T, P> extends never
? NestedObjKeyWithIndexSignatureUndefined<T, P> extends never
? P extends keyof T
? ValueWithUndefinedForIndexSignatures<T, P>
: never
: NestedObjKeyWithIndexSignatureUndefined<T, P>
: NestedArrKey<T, P>
: never
export type BaseDeepMap = Record<string, unknown>
/**
* Get a value by object path. `undefined` if key is missing.
*
* ```
* import { getPath } from 'nanostores'
*
* getPath({ a: { b: { c: ['hey', 'Hi!'] } } }, 'a.b.c[1]') // Returns 'Hi!'
* ```
*
* @param obj Any object.
* @param path Path splitted by dots and `[]`. Like: `props.arr[1].nested`.
* @returns The value for this path. Undefined if key is missing.
*/
export function getPath<T extends BaseDeepMap, K extends AllPaths<T>>(
obj: T,
path: K
): FromPath<T, K>
/**
* Set a deep value by path. Copies are made at each level of `path` so that no
* part of the original object is mutated (but it does not do a full deep copy
* -- some sub-objects may still be shared between the old value and the new
* one). Sparse arrays will be created if you set arbitrary length.
*
* ```
* import { setPath } from 'nanostores'
*
* setPath({ a: { b: { c: [] } } }, 'a.b.c[1]', 'hey')
* // Returns `{ a: { b: { c: [<empty>, 'hey'] } } }`
* ```
*
* @param obj Any object.
* @param path Path splitted by dots and `[]`. Like: `props.arr[1].nested`.
* @returns The new object.
*/
export function setPath<T extends BaseDeepMap, K extends AllPaths<T>>(
obj: T,
path: K,
value: FromPath<T, K>
): T
/**
* Set a deep value by key. Copies are made at each level of `path` so that no
* part of the original object is mutated (but it does not do a full deep copy
* -- some sub-objects may still be shared between the old value and the new
* one). Sparse arrays will be created if you set arbitrary length.
*
* ```
* import { setByKey } from 'nanostores'
*
* setByKey({ a: { b: { c: [] } } }, ['a', 'b', 'c', 1], 'hey')
* // Returns `{ a: { b: { c: [<empty>, 'hey'] } } }`
* ```
*
* @param obj Any object.
* @param splittedKeys An array of keys representing the path to the value.
* @param value New value.
* @retunts The new object.
*/
export function setByKey<T extends BaseDeepMap>(
obj: T,
splittedKeys: PropertyKey[],
value: unknown
): T

View File

@ -0,0 +1,64 @@
export function getPath(obj, path) {
let allKeys = getAllKeysFromPath(path)
let res = obj
for (let key of allKeys) {
if (res === undefined) {
break
}
res = res[key]
}
return res
}
export function setPath(obj, path, value) {
return setByKey(obj != null ? obj : {}, getAllKeysFromPath(path), value)
}
export function setByKey(obj, splittedKeys, value) {
let key = splittedKeys[0]
let copy = Array.isArray(obj) ? [...obj] : { ...obj }
if (splittedKeys.length === 1) {
if (value === undefined) {
if (Array.isArray(copy)) {
copy.splice(key, 1)
} else {
delete copy[key]
}
} else {
copy[key] = value
}
return copy
}
ensureKey(copy, key, splittedKeys[1])
copy[key] = setByKey(copy[key], splittedKeys.slice(1), value)
return copy
}
const ARRAY_INDEX = /(.*)\[(\d+)\]/
function getAllKeysFromPath(path) {
return path.split('.').flatMap(key => getKeyAndIndicesFromKey(key))
}
function getKeyAndIndicesFromKey(key) {
if (ARRAY_INDEX.test(key)) {
let [, keyPart, index] = key.match(ARRAY_INDEX)
return [...getKeyAndIndicesFromKey(keyPart), index]
}
return [key]
}
const IS_NUMBER = /^\d+$/
function ensureKey(obj, key, nextKey) {
if (key in obj) {
return
}
let isNum = IS_NUMBER.test(nextKey)
if (isNum) {
obj[key] = Array(parseInt(nextKey, 10) + 1)
} else {
obj[key] = {}
}
}

View File

@ -0,0 +1,34 @@
import type { StoreValues } from '../computed/index.d.ts'
import type { AnyStore, Store, StoreValue } from '../index.js'
interface Effect {
<OriginStore extends Store>(
stores: OriginStore,
cb: (value: StoreValue<OriginStore>) => void | VoidFunction
): VoidFunction
/**
* Subscribe for multiple stores. Also you can define cleanup function
* to call on stores changes.
*
* ```js
* const $enabled = atom(true)
* const $interval = atom(1000)
*
* const cancelPing = effect([$enabled, $interval], (enabled, interval) => {
* if (!enabled) return
* const intervalId = setInterval(() => {
* sendPing()
* }, interval)
* return () => {
* clearInterval(intervalId)
* }
* })
* ```
*/
<OriginStores extends AnyStore[]>(
stores: [...OriginStores],
cb: (...values: StoreValues<OriginStores>) => void | VoidFunction
): VoidFunction
}
export const effect: Effect

View File

@ -0,0 +1,21 @@
export let effect = (stores, callback) => {
if (!Array.isArray(stores)) stores = [stores]
let unbinds = []
let lastRunUnbind
let run = () => {
lastRunUnbind && lastRunUnbind()
let values = stores.map(store => store.get())
lastRunUnbind = callback(...values)
}
unbinds = stores.map(store => store.listen(run))
run()
return () => {
unbinds.forEach(unbind => unbind())
lastRunUnbind && lastRunUnbind()
}
}

View File

@ -0,0 +1,44 @@
export {
atom,
Atom,
PreinitializedWritableAtom,
ReadableAtom,
readonlyType,
WritableAtom
} from './atom/index.js'
export { clean, cleanStores } from './clean-stores/index.js'
export { batched, computed } from './computed/index.js'
export {
AllPaths,
BaseDeepMap,
deepMap,
DeepMapStore,
FromPath,
getKey,
getPath,
setByKey,
setPath
} from './deep-map/index.js'
export { effect } from './effect/index.js'
export { keepMount } from './keep-mount/index.js'
export {
onMount,
onNotify,
onSet,
onStart,
onStop,
STORE_UNMOUNT_DELAY
} from './lifecycle/index.js'
export { listenKeys, subscribeKeys } from './listen-keys/index.js'
export { mapCreator, MapCreator } from './map-creator/index.js'
export {
AnyStore,
map,
MapStore,
MapStoreKeys,
PreinitializedMapStore,
Store,
StoreValue,
WritableStore
} from './map/index.js'
export { allTasks, cleanTasks, startTask, task, Task } from './task/index.js'

View File

@ -0,0 +1,24 @@
export { atom, readonlyType } from './atom/index.js'
export { clean, cleanStores } from './clean-stores/index.js'
export { batched, computed } from './computed/index.js'
export {
deepMap,
getKey,
getPath,
setByKey,
setPath
} from './deep-map/index.js'
export { effect } from './effect/index.js'
export { keepMount } from './keep-mount/index.js'
export {
onMount,
onNotify,
onSet,
onStart,
onStop,
STORE_UNMOUNT_DELAY
} from './lifecycle/index.js'
export { listenKeys, subscribeKeys } from './listen-keys/index.js'
export { mapCreator } from './map-creator/index.js'
export { map } from './map/index.js'
export { allTasks, cleanTasks, startTask, task } from './task/index.js'

View File

@ -0,0 +1,17 @@
import type { MapCreator } from '../map-creator/index.js'
import type { Store } from '../map/index.js'
/**
* Prevent destructor call for the store.
*
* Together with {@link cleanStores} is useful tool for tests.
*
* ```js
* import { keepMount } from 'nanostores'
*
* keepMount($store)
* ```
*
* @param $store The store.
*/
export function keepMount($store: MapCreator | Store): void

View File

@ -0,0 +1,3 @@
export let keepMount = $store => {
$store.listen(() => {})
}

View File

@ -0,0 +1,152 @@
import type { MapStore, Store, StoreValue } from '../map/index.js'
type AtomSetPayload<Shared, SomeStore extends Store> = {
abort(): void
changed: undefined
newValue: StoreValue<SomeStore>
shared: Shared
}
type MapSetPayload<Shared, SomeStore extends Store> =
| {
abort(): void
changed: keyof StoreValue<SomeStore>
newValue: StoreValue<SomeStore>
shared: Shared
}
| AtomSetPayload<Shared, SomeStore>
type AtomNotifyPayload<Shared, SomeStore extends Store> = {
abort(): void
changed: undefined
oldValue: StoreValue<SomeStore>
shared: Shared
}
type MapNotifyPayload<Shared, SomeStore extends Store> =
| {
abort(): void
changed: keyof StoreValue<SomeStore>
oldValue: StoreValue<SomeStore>
shared: Shared
}
| AtomNotifyPayload<Shared, SomeStore>
/**
* Add listener to store chagings.
*
* ```js
* import { onSet } from 'nanostores'
*
* onSet($store, ({ newValue, abort }) => {
* if (!validate(newValue)) {
* abort()
* }
* })
* ```
*
* You can communicate between listeners by `payload.shared`
* or cancel changes by `payload.abort()`.
*
* New value of the all store will be `payload.newValue`.
* On `MapStore#setKey()` call, changed value will be in `payload.changed`.
*
* @param $store The store to add listener.
* @param listener Event callback.
* @returns A function to remove listener.
*/
export function onSet<Shared = never, SomeStore extends Store = Store>(
$store: SomeStore,
listener: (
payload: SomeStore extends MapStore
? MapSetPayload<Shared, SomeStore>
: AtomSetPayload<Shared, SomeStore>
) => void
): () => void
/**
* Add listener to notifying about store changes.
*
* You can communicate between listeners by `payload.shared`
* or cancel changes by `payload.abort()`.
*
* On `MapStore#setKey()` call, changed value will be in `payload.changed`.
*
* @param $store The store to add listener.
* @param listener Event callback.
* @returns A function to remove listener.
*/
export function onNotify<Shared = never, SomeStore extends Store = Store>(
$store: SomeStore,
listener: (
payload: SomeStore extends MapStore
? MapNotifyPayload<Shared, SomeStore>
: AtomNotifyPayload<Shared, SomeStore>
) => void
): () => void
/**
* Add listener on first store listener.
*
* We recommend to always use `onMount` instead to prevent flickering.
* See {@link onMount} to add constructor and destructor for the store.
*
* You can communicate between listeners by `payload.shared`.
*
* @param $store The store to add listener.
* @param listener Event callback.
* @returns A function to remove listener.
*/
export function onStart<Shared = never>(
$store: Store,
listener: (payload: { shared: Shared }) => void
): () => void
/**
* Add listener on last store listener unsubscription.
*
* We recommend to always use `onMount` instead to prevent flickering.
* See {@link onMount} to add constructor and destructor for the store.
*
* You can communicate between listeners by `payload.shared`.
*
* @param $store The store to add listener.
* @param listener Event callback.
* @returns A function to remove listener.
*/
export function onStop<Shared = never>(
$store: Store,
listener: (payload: { shared: Shared }) => void
): () => void
export const STORE_UNMOUNT_DELAY: number
/**
* Run constructor on first stores listener and run destructor on last listener
* unsubscription. It has a debounce to prevent flickering.
*
* A way to reduce memory and CPU usage when you do not need a store.
*
* You can communicate between listeners by `payload.shared`.
*
* ```js
* import { onMount } from 'nanostores'
*
* // Listen for URL changes on first stores listener.
* onMount($router, () => {
* parse()
* window.addEventListener('popstate', parse)
* return () => {
* window.removeEventListener('popstate', parse)
* }
* })
* ```
*
* @param $store Store to listen.
* @param initialize Store constructor. Returns store destructor.
* @return A function to remove constructor and destructor from store.
*/
export function onMount<Shared = never>(
$store: Store,
initialize?: (payload: { shared: Shared }) => (() => void) | void
): () => void

View File

@ -0,0 +1,160 @@
import { clean } from '../clean-stores/index.js'
const START = 0
const STOP = 1
const SET = 2
const NOTIFY = 3
const MOUNT = 5
const UNMOUNT = 6
const REVERT_MUTATION = 10
export let on = (object, listener, eventKey, mutateStore) => {
object.events = object.events || {}
if (!object.events[eventKey + REVERT_MUTATION]) {
object.events[eventKey + REVERT_MUTATION] = mutateStore(eventProps => {
// eslint-disable-next-line no-sequences
object.events[eventKey].reduceRight((event, l) => (l(event), event), {
shared: {},
...eventProps
})
})
}
object.events[eventKey] = object.events[eventKey] || []
object.events[eventKey].push(listener)
return () => {
let currentListeners = object.events[eventKey]
let index = currentListeners.indexOf(listener)
currentListeners.splice(index, 1)
if (!currentListeners.length) {
delete object.events[eventKey]
object.events[eventKey + REVERT_MUTATION]()
delete object.events[eventKey + REVERT_MUTATION]
}
}
}
export let onStart = ($store, listener) =>
on($store, listener, START, runListeners => {
let originListen = $store.listen
$store.listen = arg => {
if (!$store.lc && !$store.starting) {
$store.starting = true
runListeners()
delete $store.starting
}
return originListen(arg)
}
return () => {
$store.listen = originListen
}
})
export let onStop = ($store, listener) =>
on($store, listener, STOP, runListeners => {
let originOff = $store.off
$store.off = () => {
runListeners()
originOff()
}
return () => {
$store.off = originOff
}
})
export let onSet = ($store, listener) =>
on($store, listener, SET, runListeners => {
let originSet = $store.set
let originSetKey = $store.setKey
if ($store.setKey) {
$store.setKey = (changed, changedValue) => {
let isAborted
let abort = () => {
isAborted = true
}
runListeners({
abort,
changed,
newValue: { ...$store.value, [changed]: changedValue }
})
if (!isAborted) return originSetKey(changed, changedValue)
}
}
$store.set = newValue => {
let isAborted
let abort = () => {
isAborted = true
}
runListeners({ abort, newValue })
if (!isAborted) return originSet(newValue)
}
return () => {
$store.set = originSet
$store.setKey = originSetKey
}
})
export let onNotify = ($store, listener) =>
on($store, listener, NOTIFY, runListeners => {
let originNotify = $store.notify
$store.notify = (oldValue, changed) => {
let isAborted
let abort = () => {
isAborted = true
}
runListeners({ abort, changed, oldValue })
if (!isAborted) return originNotify(oldValue, changed)
}
return () => {
$store.notify = originNotify
}
})
export let STORE_UNMOUNT_DELAY = 1000
export let onMount = ($store, initialize) => {
let listener = payload => {
let destroy = initialize(payload)
if (destroy) $store.events[UNMOUNT].push(destroy)
}
return on($store, listener, MOUNT, runListeners => {
let originListen = $store.listen
$store.listen = (...args) => {
if (!$store.lc && !$store.active) {
$store.active = true
runListeners()
}
return originListen(...args)
}
let originOff = $store.off
$store.events[UNMOUNT] = []
$store.off = () => {
originOff()
setTimeout(() => {
if ($store.active && !$store.lc) {
$store.active = false
for (let destroy of $store.events[UNMOUNT]) destroy()
$store.events[UNMOUNT] = []
}
}, STORE_UNMOUNT_DELAY)
}
if (process.env.NODE_ENV !== 'production') {
let originClean = $store[clean]
$store[clean] = () => {
for (let destroy of $store.events[UNMOUNT]) destroy()
$store.events[UNMOUNT] = []
$store.active = false
originClean()
}
}
return () => {
$store.listen = originListen
$store.off = originOff
}
})
}

View File

@ -0,0 +1,75 @@
import type { StoreValue } from '../map/index.js'
/**
* Listen for specific keys of the store.
*
* In contrast with {@link subscribeKeys} it do not call listener
* immediately.
* ```js
* import { listenKeys } from 'nanostores'
*
* listenKeys($page, ['blocked'], (value, oldValue, changed) => {
* if (value.blocked) {
* console.log('You has no access')
* }
* })
* ```
*
* @param $store The store to listen.
* @param keys The keys to listen.
* @param listener Standard listener.
*/
export function listenKeys<
SomeStore extends { setKey: (key: any, value: any) => void }
>(
$store: SomeStore,
keys: SomeStore extends { setKey: (key: infer Key, value: never) => unknown }
? readonly Key[]
: never,
listener: (
value: StoreValue<SomeStore>,
oldValue: StoreValue<SomeStore>,
changed: SomeStore extends {
setKey: (key: infer Key, value: never) => unknown
}
? Key[]
: never
) => void
): () => void
/**
* Listen for specific keys of the store and call listener immediately.
* Note that the oldValue and changed arguments in the listener are
* undefined during the initial call.
*
* ```js
* import { subscribeKeys } from 'nanostores'
*
* subscribeKeys($page, ['blocked'], (value, oldValue, changed) => {
* if (value.blocked) {
* console.log('You has no access')
* }
* })
* ```
*
* @param $store The store to listen.
* @param keys The keys to listen.
* @param listener Standard listener.
*/
export function subscribeKeys<
SomeStore extends { setKey: (key: any, value: any) => void }
>(
$store: SomeStore,
keys: SomeStore extends { setKey: (key: infer Key, value: never) => unknown }
? readonly Key[]
: never,
listener: (
value: StoreValue<SomeStore>,
oldValue: StoreValue<SomeStore>,
changed: SomeStore extends {
setKey: (key: infer Key, value: never) => unknown
}
? Key[]
: never
) => void
): () => void

View File

@ -0,0 +1,14 @@
export function listenKeys($store, keys, listener) {
let keysSet = new Set(keys).add(undefined)
return $store.listen((value, oldValue, changed) => {
if (keysSet.has(changed)) {
listener(value, oldValue, changed)
}
})
}
export function subscribeKeys($store, keys, listener) {
let unbind = listenKeys($store, keys, listener)
listener($store.value)
return unbind
}

View File

@ -0,0 +1,29 @@
import type { MapStore } from '../map/index.js'
export interface MapCreator<
Value extends object = any,
Args extends any[] = []
> {
(id: string, ...args: Args): MapStore<Value>
build(id: string, ...args: Args): MapStore<Value>
cache: {
[id: string]: MapStore<{ id: string } & Value>
}
}
/**
* Create function to create map stores. It will be like a class for store.
*
* @param init Stores initializer. Returns store destructor.
*/
export function mapCreator<
Value extends object,
Args extends any[] = [],
StoreExt = Record<number | string | symbol, any>
>(
init?: (
store: MapStore<{ id: string } & Value> & StoreExt,
id: string,
...args: Args
) => (() => void) | void
): MapCreator<Value, Args>

View File

@ -0,0 +1,38 @@
import { clean } from '../clean-stores/index.js'
import { onMount } from '../lifecycle/index.js'
import { map } from '../map/index.js'
export function mapCreator(init) {
let Creator = (id, ...args) => {
if (!Creator.cache[id]) {
Creator.cache[id] = Creator.build(id, ...args)
}
return Creator.cache[id]
}
Creator.build = (id, ...args) => {
let store = map({ id })
onMount(store, () => {
let destroy
if (init) destroy = init(store, id, ...args)
return () => {
delete Creator.cache[id]
if (destroy) destroy()
}
})
return store
}
Creator.cache = {}
if (process.env.NODE_ENV !== 'production') {
Creator[clean] = () => {
for (let id in Creator.cache) {
Creator.cache[id][clean]()
}
Creator.cache = {}
}
}
return Creator
}

View File

@ -0,0 +1,149 @@
import type {
AllKeys,
ReadableAtom,
ReadonlyIfObject,
WritableAtom
} from '../atom/index.js'
type KeyofBase = keyof any
type Get<T, K extends KeyofBase> = Extract<T, { [K1 in K]: any }>[K]
export type HasIndexSignature<T> = string extends keyof T ? true : false
export type ValueWithUndefinedForIndexSignatures<
Value,
Key extends keyof Value
> = HasIndexSignature<Value> extends true ? undefined | Value[Key] : Value[Key]
export type WritableStore<Value = any> =
| (Value extends object ? MapStore<Value> : never)
| WritableAtom<Value>
export type Store<Value = any> = ReadableAtom<Value> | WritableStore<Value>
export type AnyStore<Value = any> = {
get(): Value
readonly value: undefined | Value
}
export type StoreValue<SomeStore> = SomeStore extends {
get(): infer Value
}
? Value
: any
export type BaseMapStore<Value = any> = {
setKey: (key: any, value: any) => any
} & WritableAtom<Value>
export type MapStoreKeys<SomeStore> = SomeStore extends {
setKey: (key: infer K, ...args: any[]) => any
}
? K
: AllKeys<StoreValue<SomeStore>>
export interface MapStore<Value extends object = any>
extends WritableAtom<Value> {
/**
* Subscribe to store changes.
*
* In contrast with {@link Store#subscribe} it do not call listener
* immediately.
*
* @param listener Callback with store value and old value.
* @param changedKey Key that was changed. Will present only if `setKey`
* has been used to change a store.
* @returns Function to remove listener.
*/
listen(
listener: (
value: ReadonlyIfObject<Value>,
oldValue: ReadonlyIfObject<Value>,
changedKey: AllKeys<Value>
) => void
): () => void
/**
* Low-level method to notify listeners about changes in the store.
*
* Can cause unexpected behaviour when combined with frontend frameworks
* that perform equality checks for values, such as React.
*/
notify(oldValue?: ReadonlyIfObject<Value>, changedKey?: AllKeys<Value>): void
/**
* Change store value.
*
* ```js
* $settings.set({ theme: 'dark' })
* ```
*
* Operation is atomic, subscribers will be notified once with the new value.
* `changedKey` will be undefined
*
* @param newValue New store value.
*/
set(newValue: Value): void
/**
* Change key in store value.
*
* ```js
* $settings.setKey('theme', 'dark')
* ```
*
* To delete key set `undefined`.
*
* ```js
* $settings.setKey('theme', undefined)
* ```
*
* @param key The key name.
* @param value New value.
*/
setKey<Key extends AllKeys<Value>>(
key: Key,
value: Get<Value, Key> | ValueWithUndefinedForIndexSignatures<Value, Key>
): void
/**
* Subscribe to store changes and call listener immediately.
*
* ```
* import { $router } from '../store'
*
* $router.subscribe(page => {
* console.log(page)
* })
* ```
*
* @param listener Callback with store value and old value.
* @param changedKey Key that was changed. Will present only
* if `setKey` has been used to change a store.
* @returns Function to remove listener.
*/
subscribe(
listener: (
value: ReadonlyIfObject<Value>,
oldValue: ReadonlyIfObject<Value> | undefined,
changedKey: AllKeys<Value> | undefined
) => void
): () => void
}
export interface PreinitializedMapStore<Value extends object = any>
extends MapStore<Value> {
readonly value: Value
}
/**
* Create map store. Map store is a store with key-value object
* as a store value.
*
* @param init Initialize store and return store destructor.
* @returns The store object with methods to subscribe.
*/
export function map<Value extends object, StoreExt extends object = object>(
value?: Value
): PreinitializedMapStore<Value> & StoreExt

View File

@ -0,0 +1,22 @@
import { atom } from '../atom/index.js'
export let map = (initial = {}) => {
let $map = atom(initial)
$map.setKey = function (key, value) {
let oldMap = $map.value
if (typeof value === 'undefined' && key in $map.value) {
$map.value = { ...$map.value }
delete $map.value[key]
$map.notify(oldMap, key)
} else if ($map.value[key] !== value) {
$map.value = {
...$map.value,
[key]: value
}
$map.notify(oldMap, key)
}
}
return $map
}

View File

@ -0,0 +1,34 @@
{
"name": "nanostores",
"version": "1.0.1",
"description": "A tiny (265 bytes) state manager for React/Preact/Vue/Svelte with many atomic tree-shakable stores",
"keywords": [
"store",
"state",
"state manager",
"react",
"react native",
"preact",
"vue",
"svelte"
],
"author": "Andrey Sitnik <andrey@sitnik.ru>",
"license": "MIT",
"repository": "nanostores/nanostores",
"sideEffects": false,
"type": "module",
"types": "./index.d.ts",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
},
"engines": {
"node": "^20.0.0 || >=22.0.0"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
}

View File

@ -0,0 +1,76 @@
export interface Task<Value> extends Promise<Value> {
t: true
}
/**
* Track store async task by start/end functions.
* It is useful for test to wait end of the processing.
*
* It you use `async`/`await` in task, you can use {@link task}.
*
* ```ts
* import { startTask } from 'nanostores'
*
* function saveUser () {
* const endTask = startTask()
* api.submit('/user', user.get(), () => {
* $user.setKey('saved', true)
* endTask()
* })
* }
* ```
*/
export function startTask(): () => void
/**
* Track store async task by wrapping promise callback.
* It is useful for test to wait end of the processing.
*
* ```ts
* import { task } from 'nanostores'
*
* async function saveUser () {
* await task(async () => {
* await api.submit('/user', user.get())
* $user.setKey('saved', true)
* })
* }
* ```
*
* @param cb Async callback with task.
* @return Return value from callback.
*/
export function task<Return = never>(
cb: () => Promise<Return> | Return
): Task<Return>
/**
* Return Promise until all current tasks (and tasks created while waiting).
*
* It is useful in tests to wait all async processes in the stores.
*
* ```ts
* import { allTasks } from 'nanostores'
*
* it('saves user', async () => {
* saveUser()
* await allTasks()
* expect($user.get().saved).toBe(true)
* })
* ```
*/
export function allTasks(): Promise<void>
/**
* Forget all tracking tasks. Use it only for tests.
* {@link cleanStores} cleans tasks automatically.
*
* ```js
* import { cleanTasks } from 'nanostores'
*
* afterEach(() => {
* cleanTasks()
* })
* ```
*/
export function cleanTasks(): void

View File

@ -0,0 +1,35 @@
let tasks = 0
let resolves = []
export function startTask() {
tasks += 1
return () => {
tasks -= 1
if (tasks === 0) {
let prevResolves = resolves
resolves = []
for (let i of prevResolves) i()
}
}
}
export function task(cb) {
let endTask = startTask()
let promise = cb().finally(endTask)
promise.t = true
return promise
}
export function allTasks() {
if (tasks === 0) {
return Promise.resolve()
} else {
return new Promise(resolve => {
resolves.push(resolve)
})
}
}
export function cleanTasks() {
tasks = 0
}