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

nai-store

v0.3.0

Published

A minimal synchronous state management library for NovelAI Scripting

Downloads

219

Readme

NAIStore

NAIStore is a small, synchronous state management library designed for NovelAI scripts and similar single-threaded JavaScript environments.

It provides a predictable way to manage application state using:

  • Reducers (and Slices)
  • Actions
  • Selector-based subscriptions
  • Explicit side effects (Effects)

NAIStore is intentionally minimal, dependency-free, and safe to embed directly into a script without build tooling.

Installation

Method A: Copy-paste (simplest)

Copy src/nai-store.ts directly into your NovelAI Script project.

Method B: npm + nibs

If your project uses nibs or another bundler that resolves node_modules:

npm install nai-store
import { createStore, createSlice, combineReducers, matchesAction } from "nai-store";

Note: This package distributes raw TypeScript source — no compilation step is needed. Your bundler must support .ts imports.


Why NAIStore?

NovelAI scripts often need to manage state such as:

  • Configuration options
  • Script modes
  • UI selections
  • Message lists
  • Persistent counters

NAIStore exists to make that state:

  • Centralized
  • Predictable
  • Easy to reason about
  • Cheap to observe

It avoids abstractions that are unnecessary or harmful in a scripting environment.


Design Principles

NAIStore is built around a few strict principles:

  • State updates are synchronous
  • Effects are synchronously invoked
  • Reducers are pure
  • Subscriptions are selector-based
  • Side effects are explicit
  • No framework or rendering assumptions

This makes NAIStore suitable for:

  • UI scripts
  • Background logic
  • Generator hooks
  • Mono-script environments

Installation

NAIStore is designed to be copied directly into your script.

There is no required module system, bundler, or runtime dependency.


Performance Notes

NAIStore is synchronous and efficient, but it is not designed for high-frequency updates.

Do not dispatch actions for:

  • Token streams
  • Partial text updates
  • Animation frames
  • Rapid progress updates

Handle those imperatively instead.


Core Concepts

Actions

Actions are plain objects that describe what happened. They typically have a type and a payload.

type Action<T = string> = {
  type: T;
  payload?: unknown;
};

Reducers

Reducers are pure functions that compute the next state based on the previous state and an action.

type Reducer<S> = (state: S | undefined, action: Action) => S;

Creating a Store

const store = createStore(reducer);

A store owns:

  • The current state
  • The reducer
  • Subscriptions
  • Effects

An optional debug flag enables action logging:

const store = createStore(reducer, true);
// Logs every dispatched action via api.v1.log

Modern Usage: createSlice

While you can write reducers manually with switch statements, NAIStore provides createSlice to generate reducers and actions automatically. This reduces boilerplate and ensures type safety.

const counterSlice = createSlice({
  name: "counter",
  initialState: 0,
  reducers: {
    increment: (state) => state + 1,
    decrement: (state) => state - 1,
    add: (state, amount: number) => state + amount,
  },
});

const { actions, reducer } = counterSlice;

// actions.increment() -> { type: 'counter/increment', payload: undefined }
// actions.add(5)      -> { type: 'counter/add', payload: 5 }

Alternative: createReducer

For cases where you want a map-based reducer without auto-generated action creators, use createReducer:

type MyAction =
  | { type: "INC" }
  | { type: "ADD"; amount: number };

const reducer = createReducer<number, MyAction>(0, {
  INC: (state) => state + 1,
  ADD: (state, action) => state + action.amount,
});

This is useful when you need custom action shapes or want to handle actions from multiple sources.


Reading State

const state = store.getState();

This is synchronous and side-effect free.


Dispatching Actions

store.dispatch(actions.increment());

Dispatching an action:

  1. Runs the reducer
  2. Updates state if it changed
  3. Notifies selector subscribers
  4. Runs matching effects

Reducer execution, subscriptions, and effect invocation all happen synchronously and in order.


Selector Subscriptions (Reactive Logic)

NAIStore supports selector-based subscriptions. They take a pair of functions as arguments. The first function is the selector. Its job is to select, collect, or reduce values from the store. When the result changes during an action dispatch, the listener will be called with the selection.

store.subscribeSelector(
  (state) => state.count,
  (count) => {
    api.v1.log("Count is now:", count);
  },
);

Change Detection

When you subscribe, the selector is evaluated immediately to capture the initial value. On subsequent dispatches, the selector is re-evaluated and the listener is called only if the new value differs from the previous one (compared using Object.is by default).

This means the listener is not called at subscription time — only on future state changes that produce a new selected value.

Custom Equality

For selectors that return derived values (arrays, objects) where reference equality would fire on every dispatch, supply a custom equals function as the third argument:

store.subscribeSelector(
  (state) => state.items.map((item) => item.id),
  (ids) => { /* rebuild UI */ },
  (a, b) => a.length === b.length && a.every((k, i) => k === b[i]),
);

The listener will only fire when the key sequence actually changes.


Effects (Side Effects)

Effects allow you to respond to actions with imperative behavior. Unlike reducers, effects can be impure.

store.subscribeEffect(
  // 1. Predicate: When to run
  (action) => action.type === 'SAVE',

  // 2. Effect: What to do
  (action, ctx) => {
    api.v1.storage.set("state", ctx.getState());
  },
);

Effect execution rules

Effects:

  • Run after the reducer completes
  • Run synchronously
  • Run for every dispatched action that matches the predicate
  • Can dispatch new actions

Note: NAIStore includes a dispatch cascade guard. If effects dispatch further actions that themselves dispatch further actions, the depth is tracked. If depth exceeds 10, the action is dropped and a warning is logged. This prevents runaway infinite dispatch loops.


Action Matching with matchesAction

The matchesAction helper provides type-safe action matching for use with subscribeEffect. It extracts the action type from a slice action creator and returns a type guard.

store.subscribeEffect(
  matchesAction(todosSlice.actions.add),
  (action, ctx) => {
    // action is typed as PayloadAction<AddPayload>
    api.v1.log("Todo added:", action.payload);
  },
);

Payload Predicate

You can optionally provide a predicate to match only specific payloads:

store.subscribeEffect(
  matchesAction(todosSlice.actions.toggle, (payload) => payload.id === "special"),
  (action, ctx) => {
    // Only fires when the "special" todo is toggled
  },
);

Examples

Example 1: Simple Counter (using createSlice)

This example demonstrates the core data flow with minimal boilerplate.

// 1. Define the Slice
const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => ({ value: state.value + 1 }),
    decrement: (state) => ({ value: state.value - 1 }),
    reset: () => ({ value: 0 }),
  },
});

// 2. Create Store
const store = createStore(counterSlice.reducer);

// 3. Subscribe
store.subscribeSelector(
  (state) => state.value,
  (val) => api.v1.log(`Counter: ${val}`),
);

// 4. Dispatch
const { increment, decrement, reset } = counterSlice.actions;

store.dispatch(increment()); // Counter: 1
store.dispatch(increment()); // Counter: 2
store.dispatch(decrement()); // Counter: 1
store.dispatch(reset());     // Counter: 0

Example 2: Complex TODO List (Full Architecture)

This example demonstrates:

  • TypeScript type inference for State and Actions
  • createSlice for feature-based logic
  • combineReducers for composing state
  • Effects for persistence
  • matchesAction for type-safe effect predicates
  • Handling complex data structures

1. Define Types and Slices

// --- Domain Types ---
type Todo = { id: string; text: string; done: boolean };
type Filter = "all" | "active" | "completed";

// --- Todos Slice ---
const todosSlice = createSlice({
  name: "todos",
  initialState: {
    items: [] as Todo[],
    filter: "all" as Filter,
  },
  reducers: {
    add: (state, todo: Todo) => ({
      ...state,
      items: [...state.items, todo],
    }),
    toggle: (state, id: string) => ({
      ...state,
      items: state.items.map((t) =>
        t.id === id ? { ...t, done: !t.done } : t
      ),
    }),
    setFilter: (state, filter: Filter) => ({
      ...state,
      filter,
    }),
    // Bulk load for hydration
    load: (state, items: Todo[]) => ({
      ...state,
      items,
    }),
  },
});

// Extract actions for ease of use
const { add, toggle, setFilter, load } = todosSlice.actions;

2. Compose the Store

// Combine reducers (extensible for more features)
const rootReducer = combineReducers({
  todos: todosSlice.reducer,
  // settings: settingsSlice.reducer,
});

// Infer RootState from the reducer itself
type RootState = ReturnType<typeof rootReducer>;

const store = createStore(rootReducer);

3. Persistence Effect

// Persist whenever a todo is added or toggled
store.subscribeEffect(
  (action) =>
    [add.type, toggle.type].includes(action.type),

  (_action, ctx) => {
    api.v1.storage.set("todos", ctx.getState())
    .catch((err) => api.v1.error("Failed to save todos:", err))
  }
);

4. Usage

// Subscribe to filtered view
store.subscribeSelector(
  (state) => {
    const { items, filter } = state.todos;
    if (filter === "all") return items;
    return items.filter((t) => (filter === "completed" ? t.done : !t.done));
  },
  (visibleTodos) => {
    api.v1.log("Visible Todos Updated:", visibleTodos.map((t) => t.text));
  }
);

// Dispatch Actions
store.dispatch(add({ id: "1", text: "Learn NAIStore", done: false }));
store.dispatch(add({ id: "2", text: "Build something cool", done: false }));
store.dispatch(toggle("1"));

store.dispatch(setFilter("active"));
// Listener fires with only active todos

What this shows:

  • Type Safety: RootState is inferred, Payload types are enforced by createSlice.
  • Modularity: Logic is encapsulated in slices.
  • Predictability: Data flows one way: Action -> Reducer -> Store -> Selectors.