npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

devlink-statecore

v1.0.3

Published

Minimalist framework-agnostic state manager for JavaScript/TypeScript. Zero dependencies, immutable updates, middleware support, computed state. Works in any JS runtime.

Downloads

37

Readme

DevLink StateCore

Минималистичный фреймворк-агностичный state-менеджер для JavaScript и TypeScript.

Ноль зависимостей. Иммутабельные обновления. Middleware. Computed state. Работает везде.

Содержание

Возможности

  • Фреймворк-агностичность — работает в любом JS-окружении: браузер, Node.js, Deno, Bun
  • Ноль зависимостей — ядро не использует внешних библиотек
  • TypeScript из коробки — полная типобезопасность, все типы экспортируются
  • Иммутабельное состояниеObject.freeze при каждом обновлении
  • Middleware — Redux-подобный compose pattern для логирования, персистенции, devtools
  • Computed/derived state — ленивое вычисление с кешированием зависимостей
  • Батчинг уведомлений — несколько синхронных обновлений вызывают один listener
  • Селекторы — подписка на конкретный срез состояния, без лишних перерисовок
  • Компактность — ядро ~200 строк, минифицированный бандл ~1.8 KB

Установка

npm install devlink-statecore

Быстрый старт

import { createStore } from "devlink-statecore";

// 1. Создаём стор
const store = createStore({
  count: 0,
  user: "anonymous",
});

// 2. Подписываемся на изменения
const unsubscribe = store.subscribe((state, prevState) => {
  console.log("Было:", prevState.count, "→ Стало:", state.count);
});

// 3. Обновляем состояние
store.setState({ count: 1 });
store.setState((s) => ({ count: s.count + 1 }));

// 4. Читаем состояние
console.log(store.getState()); // { count: 2, user: "anonymous" }

// 5. Отписываемся
unsubscribe();

API ядра

createStore<T>(initialState, options?)

Создаёт новый стор. Это единственная фабрика — все остальные методы доступны через возвращённый объект.

Параметры:

| Параметр | Тип | Описание | | --- | --- | --- | | initialState | T | Начальное состояние (объект). Будет заморожен через Object.freeze | | options?.name | string | Имя стора. Используется в ошибках и devtools. По умолчанию "DevLinkStore" | | options?.middleware | Middleware<T>[] | Начальная цепочка middleware |

Возвращает: Store<T> — объект стора со всеми методами.

import { createStore } from "devlink-statecore";

// Без опций
const store = createStore({ count: 0, name: "test" });

// С опциями
import { logger } from "devlink-statecore/middleware";

const store = createStore(
  { count: 0, name: "test" },
  {
    name: "AppStore",
    middleware: [logger()],
  },
);

store.getState()

Возвращает текущее состояние стора. Состояние заморожено (Object.freeze) — любая попытка мутации выбросит ошибку.

Параметры: нет.

Возвращает: Readonly<T> — замороженный объект текущего состояния.

const state = store.getState();
console.log(state.count); // 0

// Мутация выбросит TypeError
state.count = 5; // TypeError: Cannot assign to read only property

store.setState(updater)

Обновляет состояние стора. Поддерживает два формата: частичный объект или функцию-updater. Выполняет shallow merge с текущим состоянием. Если результат shallow-equal текущему — обновление пропускается.

Параметры:

| Параметр | Тип | Описание | | --- | --- | --- | | updater | Partial<T> | Частичный объект, поля которого будут merged в текущее состояние | | updater | (state: T) => Partial<T> | Функция, принимающая текущее состояние и возвращающая partial-обновление |

Возвращает: void.

Выбрасывает: Error, если стор уничтожен (destroy()).

// Обновление через partial-объект
store.setState({ count: 10 });

// Обновление через функцию (имеет доступ к актуальному state)
store.setState((state) => ({ count: state.count + 1 }));

// Можно обновлять несколько полей за раз
store.setState({ count: 5, name: "updated" });

// Shallow-equal обновление пропускается (listeners не вызываются)
store.setState({ count: 0 }); // если count уже 0 — no-op

store.subscribe(listener, selector?)

Подписывает listener на изменения состояния. Нотификации батчатся через queueMicrotask: несколько синхронных setState вызовут listener один раз.

Параметры:

| Параметр | Тип | Описание | | --- | --- | --- | | listener | (state: T, prevState: T) => void | Функция-обработчик. Получает новое и предыдущее состояние | | selector | (state: T) => R (необязательный) | Функция-селектор. Если передана, listener вызывается только когда значение selector(state) изменилось (сравнение через Object.is) |

Возвращает: () => void — функция отписки. Вызов idempotent.

Выбрасывает: Error, если стор уничтожен (destroy()).

// Подписка на все изменения
const unsub = store.subscribe((state, prevState) => {
  console.log("Было:", prevState, "→ Стало:", state);
});

// Подписка с селектором — вызывается только при изменении count
const unsub2 = store.subscribe(
  (state, prevState) => {
    console.log("count изменился:", prevState.count, "→", state.count);
  },
  (s) => s.count,
);

// Батчинг: 3 setState → 1 вызов listener
store.setState({ count: 1 });
store.setState({ count: 2 });
store.setState({ count: 3 });
// listener получит: prevState.count === 0, state.count === 3

// Отписка
unsub();
unsub2();

store.use(middleware)

Динамически подключает middleware после создания стора. Middleware встраивается в конец существующей цепочки.

Параметры:

| Параметр | Тип | Описание | | --- | --- | --- | | middleware | Middleware<T> | Функция middleware (формат: (api) => (next) => (updater) => void) |

Возвращает: void.

Выбрасывает: Error, если стор уничтожен (destroy()).

import { logger } from "devlink-statecore/middleware";

const store = createStore({ count: 0 });

// Подключаем middleware после создания
store.use(logger({ name: "Late" }));

store.setState({ count: 1 }); // middleware теперь активен

store.computed(name, definition)

Регистрирует вычисляемое (derived) значение. Значение ленивое — вычисляется только при вызове геттера. Результат кешируется до тех пор, пока зависимости не изменятся (сравнение через Object.is).

Параметры:

| Параметр | Тип | Описание | | --- | --- | --- | | name | string | Уникальное имя computed-значения | | definition.deps | ((state: T) => unknown)[] | Массив селекторов-зависимостей. Каждый извлекает одну зависимость из state | | definition.compute | (...args: unknown[]) => R | Функция вычисления. Получает текущие значения зависимостей в том же порядке |

Возвращает: () => R — геттер-функция. Каждый вызов проверяет deps и при необходимости пересчитывает.

const store = createStore({ price: 100, quantity: 3, tax: 0.2 });

// Одна зависимость
const doubled = store.computed("doubled", {
  deps: [(s) => s.price],
  compute: (price) => (price as number) * 2,
});

console.log(doubled()); // 200 (вычислено)
console.log(doubled()); // 200 (из кеша, compute не вызван)

store.setState({ price: 50 });
console.log(doubled()); // 100 (пересчитано)

// Несколько зависимостей
const total = store.computed("total", {
  deps: [(s) => s.price, (s) => s.quantity, (s) => s.tax],
  compute: (price, qty, tax) => {
    const subtotal = (price as number) * (qty as number);
    return subtotal + subtotal * (tax as number);
  },
});

console.log(total()); // 360

// Без зависимостей — вычисляется один раз и кешируется навсегда
const version = store.computed("version", {
  deps: [],
  compute: () => "1.0.0",
});

store.destroy()

Уничтожает стор: очищает все подписки, middleware, computed-записи. После вызова setState, subscribe, use будут выбрасывать ошибку. getState продолжает возвращать последнее состояние. Повторный вызов destroy безопасен (idempotent).

Параметры: нет.

Возвращает: void.

const store = createStore({ count: 0 });
const unsub = store.subscribe(() => console.log("update"));

store.destroy();

store.getState(); // { count: 0 } — работает
store.setState({ count: 1 }); // Error: [DevLinkStore] Store is destroyed
store.subscribe(() => {}); // Error: [DevLinkStore] Store is destroyed

Middleware

Middleware перехватывает вызовы setState. Формат — Redux-подобная тройная стрелка: (api) => (next) => (updater) => void.

Импорт встроенных middleware:

import { logger, devtools, persist } from "devlink-statecore/middleware";

logger(options?)

Логирует каждое обновление состояния в консоль: предыдущее и новое состояние, сгруппированные в console.group.

Параметры:

| Опция | Тип | По умолчанию | Описание | | --- | --- | --- | --- | | collapsed | boolean | true | trueconsole.groupCollapsed, falseconsole.group | | name | string | "DevLink" | Префикс в заголовке группы |

import { logger } from "devlink-statecore/middleware";

const store = createStore(
  { count: 0 },
  { middleware: [logger({ name: "MyApp", collapsed: false })] },
);

store.setState({ count: 1 });
// Консоль:
// ▼ [MyApp] state update
//   prev state: { count: 0 }
//   next state: { count: 1 }

devtools(options?)

Записывает полную историю состояний в globalThis.__DEVLINK_DEVTOOLS__. Каждая запись содержит snapshot состояния, timestamp и тип действия.

Параметры:

| Опция | Тип | По умолчанию | Описание | | --- | --- | --- | --- | | name | string | "DevLinkStore" | Ключ стора в devtools | | maxHistory | number | 50 | Максимум записей в истории. Старые удаляются (FIFO) |

Структура globalThis.__DEVLINK_DEVTOOLS__:

{
  stores: Map<string, {
    history: Array<{
      state: T;
      timestamp: number;
      action: "@@INIT" | "partial update" | "functional update";
    }>;
    currentIndex: number;
  }>;
}
import { devtools } from "devlink-statecore/middleware";

const store = createStore(
  { count: 0 },
  { middleware: [devtools({ name: "Counter", maxHistory: 100 })] },
);

store.setState({ count: 1 });
store.setState((s) => ({ count: s.count + 1 }));

// Просмотр истории
const dt = globalThis.__DEVLINK_DEVTOOLS__;
const history = dt?.stores.get("Counter")?.history;
console.log(history);
// [
//   { state: { count: 0 }, action: "@@INIT", timestamp: ... },
//   { state: { count: 1 }, action: "partial update", timestamp: ... },
//   { state: { count: 2 }, action: "functional update", timestamp: ... },
// ]

persist(options)

Сохраняет состояние в storage (по умолчанию localStorage) при каждом обновлении. При создании стора восстанавливает ранее сохранённое состояние.

Параметры:

| Опция | Тип | По умолчанию | Описание | | --- | --- | --- | --- | | key | string | (обязательный) | Ключ в storage | | storage | Storage | localStorage | Объект storage (localStorage, sessionStorage, кастомный). В Node.js/SSR — undefined (не падает) | | serialize | (state: T) => string | JSON.stringify | Функция сериализации | | deserialize | (raw: string) => Partial<T> | JSON.parse | Функция десериализации | | partialize | (state: T) => Partial<T> | undefined | Если задана, сохраняет только указанные поля |

import { persist } from "devlink-statecore/middleware";

// Базовое использование
const store = createStore(
  { count: 0, token: "secret" },
  { middleware: [persist({ key: "app-state" })] },
);

// Сохранять только count (без token)
const store2 = createStore(
  { count: 0, token: "secret" },
  {
    middleware: [
      persist({
        key: "app-safe",
        partialize: (state) => ({ count: state.count }),
      }),
    ],
  },
);

// sessionStorage вместо localStorage
const store3 = createStore(
  { count: 0 },
  {
    middleware: [
      persist({ key: "session-data", storage: sessionStorage }),
    ],
  },
);

Свой middleware

Middleware — функция с сигнатурой (api) => (next) => (updater) => void.

  • api — объект с методами getState(), setState(), subscribe()
  • next — вызов следующего middleware в цепочке (или coreSetState если последний)
  • updater — переданный в setState аргумент
import type { Middleware } from "devlink-statecore";

// Middleware, который логирует время выполнения
const timing: Middleware<MyState> = (api) => (next) => (updater) => {
  const start = performance.now();
  next(updater);
  const ms = (performance.now() - start).toFixed(2);
  console.log(`setState: ${ms}ms`);
};

// Middleware, который блокирует обновления по условию
const guard: Middleware<MyState> = (api) => (next) => (updater) => {
  const resolved =
    typeof updater === "function" ? updater(api.getState()) : updater;
  if (resolved.count !== undefined && resolved.count < 0) {
    console.warn("Отрицательный count заблокирован");
    return; // не вызываем next — обновление заблокировано
  }
  next(updater);
};

// Middleware, который трансформирует обновление
const clamp: Middleware<MyState> = () => (next) => (updater) => {
  if (typeof updater === "object" && "count" in updater) {
    next({ ...updater, count: Math.min(100, updater.count as number) });
  } else {
    next(updater);
  }
};

const store = createStore({ count: 0 }, { middleware: [timing, guard, clamp] });

Адаптеры для фреймворков

Адаптеры — отдельные entry points. Устанавливаются из того же пакета, но требуют соответствующий фреймворк в проекте.


React

Импорт: devlink-statecore/react

Требования: React 18+ (используется useSyncExternalStore).

useStore(store): Readonly<T>

Возвращает полное состояние. Компонент перерисовывается при любом изменении.

useStore(store, selector): R

Возвращает результат selector(state). Компонент перерисовывается только при изменении selected-значения.

import { useStore } from "devlink-statecore/react";
import { store } from "./store";

function Counter() {
  // Только count — перерисовка только при изменении count
  const count = useStore(store, (s) => s.count);

  return (
    <div>
      <span data-testid="count">{count}</span>
      <button
        data-testid="increment"
        onClick={() => store.setState((s) => ({ count: s.count + 1 }))}
      >
        +1
      </button>
    </div>
  );
}

function UserInfo() {
  // Полное состояние — перерисовка при любом изменении
  const state = useStore(store);
  return <span>{state.user}</span>;
}

Vue 3

Импорт: devlink-statecore/vue

Требования: Vue 3+ (используется ref, readonly, onUnmounted).

useStore(store): DeepReadonly<Ref<T>>

Возвращает реактивный Ref с полным состоянием.

useStore(store, selector): Readonly<Ref<R>>

Возвращает реактивный Ref с результатом селектора.

<script setup lang="ts">
import { useStore } from "devlink-statecore/vue";
import { store } from "./store";

// Только count
const count = useStore(store, (s) => s.count);

// Полное состояние
const state = useStore(store);

function increment() {
  store.setState((s) => ({ count: s.count + 1 }));
}
</script>

<template>
  <div>
    <span>{{ count }}</span>
    <button @click="increment">+1</button>
  </div>
</template>

Svelte

Импорт: devlink-statecore/svelte

Svelte использует store contract: объект с методом subscribe(run) => unsubscribe. Адаптер toReadable создаёт совместимый объект.

toReadable(store): SvelteReadable<Readonly<T>>

Оборачивает стор в Svelte-readable. Доступен через $-синтаксис.

toReadable(store, selector): SvelteReadable<R>

Оборачивает стор с селектором.

<script>
  import { toReadable } from "devlink-statecore/svelte";
  import { store } from "./store";

  const count = toReadable(store, (s) => s.count);
  const state = toReadable(store);

  function increment() {
    store.setState((s) => ({ count: s.count + 1 }));
  }
</script>

<span>{$count}</span>
<span>{$state.user}</span>
<button on:click={increment}>+1</button>

Vanilla JS

Импорт: devlink-statecore/vanilla

Хелперы для работы с DOM и отслеживания срезов состояния без фреймворка.

bind(store, root, mapping): () => void

Привязывает состояние стора к DOM-элементам. При каждом обновлении находит элементы через querySelector и устанавливает textContent.

Параметры:

| Параметр | Тип | Описание | | --- | --- | --- | | store | Store<T> | Стор | | root | Element | Корневой DOM-элемент для поиска | | mapping | Record<string, (state: T) => string> | Объект: CSS-селектор → функция, возвращающая текст |

Возвращает: () => void — функция отписки.

import { bind } from "devlink-statecore/vanilla";

const unsub = bind(store, document.getElementById("app")!, {
  ".count": (s) => String(s.count),
  ".user-name": (s) => s.user,
  "[data-testid='status']": (s) => (s.count > 0 ? "active" : "idle"),
});

// Отписка от обновлений DOM
unsub();

watchSelector(store, selector, callback): () => void

Следит за конкретным срезом состояния. Вызывает callback только когда значение selector(state) изменяется (сравнение через Object.is).

Параметры:

| Параметр | Тип | Описание | | --- | --- | --- | | store | Store<T> | Стор | | selector | (state: T) => R | Функция-селектор, извлекающая нужный срез | | callback | (value: R, prevValue: R) => void | Обработчик. Получает новое и предыдущее значение |

Возвращает: () => void — функция отписки.

import { watchSelector } from "devlink-statecore/vanilla";

// Следить за count
const unsub = watchSelector(
  store,
  (s) => s.count,
  (next, prev) => {
    console.log(`count: ${prev} → ${next}`);
  },
);

// Следить за derived-значением
const unsub2 = watchSelector(
  store,
  (s) => s.count > 10,
  (isAbove) => {
    document.body.classList.toggle("highlight", isAbove);
  },
);

unsub();
unsub2();

Типы TypeScript

Все типы экспортируются из основного entry point:

import type {
  Store,
  StoreOptions,
  Listener,
  Selector,
  Updater,
  SetState,
  GetState,
  Subscribe,
  Middleware,
  MiddlewareAPI,
  MiddlewareNext,
  ComputedDef,
} from "devlink-statecore";

| Тип | Описание | | --- | --- | | Store<T> | Интерфейс стора со всеми методами | | StoreOptions<T> | Опции createStore: { name?, middleware? } | | Listener<T> | (state: T, prevState: T) => void | | Selector<T, R> | (state: T) => R | | Updater<T> | Partial<T> \| ((state: T) => Partial<T>) | | SetState<T> | (updater: Updater<T>) => void | | GetState<T> | () => Readonly<T> | | Subscribe<T> | (listener: Listener<T>) => () => void | | Middleware<T> | (api: MiddlewareAPI<T>) => (next: MiddlewareNext<T>) => (updater: Updater<T>) => void | | MiddlewareAPI<T> | { getState, setState, subscribe } — объект, доступный middleware | | MiddlewareNext<T> | (updater: Updater<T>) => void — вызов следующего middleware | | ComputedDef<T, R> | { deps: Selector<T, unknown>[], compute: (...args) => R } |

Типы middleware:

import type { LoggerOptions } from "devlink-statecore/middleware";
import type { DevToolsOptions } from "devlink-statecore/middleware";
import type { PersistOptions } from "devlink-statecore/middleware";

Лицензия

MIT. Copyright (c) 2026 ООО «Айти Дев Линк».

Автор: Всеволод Белогрудов.