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

@pensarfeo/betterstore

v2.1.4

Published

Reactive store utilities with validation, persistence, and cross-tab sync

Readme

@pensarfeo/betterstore

Reactive store utilities built around a small writable core (Svelte-style change detection), with optional validation, persistence, and cross-context sync via BroadcastChannel.

Features

  • Core writable: set, get, observe, subscribe, pause/resume batching-friendly notifications
  • Store: input/output transforms, sync validation, async requestUpdate, persistence (main thread), multi-tab/worker sync
  • BasicStore / BasicToggleable: minimal layer when you only need observe / subscribe / value (subpath import)
  • Composition: Derived, GlobalStore, Selectable, Observer, Subscriber

Installation

npm install @pensarfeo/betterstore

Install Svelte alongside this package when you use Observable.bind() (the library imports onDestroy from svelte at runtime). Svelte is not listed as a peerDependency in package.json yet—you must add it to your app.

Deep imports (e.g. store/core.js) work in this repo layout; published installs depend on those files being included in the package. There is no exports map in package.json today.

Quick start

import { Store } from '@pensarfeo/betterstore'

const count = new Store(0)

count.observe((value) => {
  console.log('Count:', value)
}).run()

count.value = 10

Entry point is store.js (package.json "main").

Exports: Store, Toggleable, Derived, GlobalStore, Selectable, Observer, Subscriber, partialSubscriber.


Core API (store/core.js)

Low-level primitive used by everything else. Matches Svelte’s “notify when meaningfully different” rule via safe_not_equal.

import { writable, safe_not_equal } from '@pensarfeo/betterstore/store/core.js'

safe_not_equal(a, b)

Returns true when a and b should be treated as different for reactive updates (including NaN, objects, and functions). This is the same idea as Svelte’s store inequality check.

writable(initialValue)

Returns a plain object (not a Store instance):

| Member | Description | |--------|-------------| | get() | Current value. | | set(value) | Updates only if safe_not_equal(current, value), then notifies subscribers with (newValue, previousValue). | | observe(run, invalidate?) | Adds a subscriber. run(newValue, prevValue) is called after each effective set. invalidate (default no-op) runs before the flush queue for that subscriber. Returns an unsubscribe function. Does not call run immediately. | | subscribe(run, invalidate?) | Same as observe, but calls run(current) once when subscribing. Returns an unsubscribe function. | | run | Internal dispatch function used by set / resume; advanced use only. | | pause() | Replace notifications with a no-op until resume / unpause. | | resume() | Restore default notifier and immediately emit once with (current, prev). | | unpause() | Restore default notifier without an immediate emit. |

Use Store / BasicStore observe + Observable.run() when you want “subscribe without an immediate call until you run”.


Observable (store/basic.js)

Store and BasicStore return this from observe(fn):

const ob = store.observe((v) => console.log(v))
ob.run()   // invoke callback once with current `store.value`, chainable
ob.bind()  // register `onDestroy(() => unsub)` in a Svelte component
ob.unsubscribe()

BasicStore also exposes subscribe from writable (immediate call + ongoing updates).

On Store, subscribe(fn) is a convenience one-shot: it runs observe(fn).run() and then unsubscribes. Use observe(...).run() for a lasting subscription.


BasicStore / BasicToggleable (store/basic.js)

No validation, mutations, or async update path—only the core writable plus value, observe, and subscribe.

import { BasicStore, BasicToggleable } from '@pensarfeo/betterstore/store/basic.js'

const x = new BasicStore(0)
x.observe(fn).run()
// or: x.subscribe(fn)  — runs fn immediately, stays subscribed

BasicToggleable exposes toggle, makeTrue, makeFalse, and a read-only value getter (uses underlying set for writes).


Store (main Store constructor)

import { Store, partialSubscriber } from '@pensarfeo/betterstore'

const store = new Store(initialValue, {
  name: 'my-store',       // used for persistence key + multi-process channel id
  persist: true,          // main-thread localStorage only (see below)
  multiProcess: true,     // BroadcastChannel; browser main + worker (not Node)
})

Copied from core writable

set, get, observe, subscribe, run, pause, resume, unpause are copied onto the instance. Calling set(value) updates the raw cell and does not go through validateInput / inputMutation / validate (unlike assigning store.value).

value

  • Get: outputMutation(get())
  • Set: runs validateInputinputMutationvalidate → underlying set if allowed.

Mutations

  • mutateInput(fn) — replace input transform (single function, not a stack).
  • mutateOutput(fn) — replace output transform when reading value.
  • clearMutations() — reset both to identity.

Validation

  • addInputValidation(fn)(newValue, currentValue) => truthy/falsey; runs on value assignment only (not on raw set()).
  • addValidation(fn) — runs after input mutation, before store update; same (newValue, currentValue) shape.
  • validate, validateInput — combined validators (updated as you add rules).

Note: requestUpdate runs async validators and validate, but not validateInput. Use the same shape of checks in async validators if you need them there too.

Async update

  • addAsyncInputValidation(fn) / deleteAsyncInputValidation(fn)fn(args) may return a Promise; false aborts the update.
  • requestUpdate(args, beforeUpdate?) — sets updating true, runs async validations, then inputMutation, then validate, then optional beforeUpdate(mutated) and set(mutated). Returns a boolean promise result (true if the value was applied).
  • updatingBasicToggleable flag while requestUpdate is in flight. Uses writable.subscribe (ongoing), not Store.subscribe (one-shot).

Object helpers

  • entry(key, value) — mutates object state in place (same reference) then assigns through value.
  • delete(key) — removes a key on the current object via the same path as value.

Subscription helpers

  • partialSubscriber(action, keys, fn) — builds a partialSubscriber(keys, fn) callback and passes it to this[action], e.g. 'observe'.

Utilities

  • addMethod(name, fn), addAttribute(name, value)
  • unpersist() — stop persistence and remove the localStorage key (no-op if persistence was not enabled).

Persistence

Enabled when persist: true, env === 'main' (browser window), and localStorage is available. Uses name as the key; JSON round-trip must match the initial value’s typeof to be restored.

Multi-process

When multiProcess: true, name is required, and env is not 'node'. Uses two BroadcastChannel names derived from name (STOREBROADCAST_GET + STOREBROADCAST_POST + name). Both main and worker attach with observe(sendMessage).run(); workers also post on the request channel to pull the current value.

MultiProcess() returns a dispose function, but Store does not keep or expose it—there is no store.disposeMultiProcess() on instances.

comlink is listed in package.json dependencies for experimental code under store/multiProcess/interface.js. That path is not wired to Store’s multiProcess option (which uses store/multiProcess.js only).


Toggleable

new Toggleable(initialBoolean, options) — same options as Store. Boolean helpers: toggle, makeTrue / on, makeFalse / off, changeTo. Read-only value getter; writes go through setFn.

Async: requestToggle(), requestTrue(), requestFalse() delegate to requestUpdate. On success, requestTrue / requestFalse also call makeTrue / makeFalse after requestUpdate has already applied the value (redundant but harmless).


Derived

import { Derived } from '@pensarfeo/betterstore'

const d = new Derived([storeA, storeB], configOptional)
  • Subscribes to each source with .observe(...).run().
  • Internal value is an array of source values by index (d.get()[a, b, ...]).
  • unsubscribe() — stop listening to all sources.
  • bind() — forwards to each source subscription’s Observable.bind() (instance method, not a static on Derived).

There is no Derived.bind static helper in this package.


GlobalStore

import { GlobalStore } from '@pensarfeo/betterstore'

const s = GlobalStore(initialValue, {
  name: 'required',
  context: 'optional-namespace',
  persist: false,
  multiProcess: false,
})

First call for a given context + name creates new Store(...); later calls return the same instance. Initial value on later calls is ignored once the store exists.


Selectable

import { Selectable } from '@pensarfeo/betterstore'

const sel = new Selectable(false) // initial value for each toggleable
const a = sel.add('a')
const b = sel.add('b')
sel.selected // Store holding the selected name or null
sel.bind()
sel.unsubscribe()

Each add(name) returns a Toggleable. The intended behavior is radio-group selection: turning one on updates selected and turns the previous selection off.

See Known limitationsSelectable is not reliable with the current Store.subscribe implementation.


Observer

Event bus built on an internal Store holding the latest emit payload:

import { Observer } from '@pensarfeo/betterstore'

const bus = new Observer('events')
bus.observe((...args) => console.log(...args))
bus.emit(1, 2)
bus.once((x) => console.log('once', x))
  • observe(fn) — returns an Observable; fn receives spread arguments from the last emit. .run() is optional and only needed for a synchronous read before the first emit.
  • once(fn) — same as observe, unsubscribes after the first notification.
  • emit(...args) — notifies observers with those arguments.

Subscriber

Event bus like Observer, backed by an internal Store whose value is { payload: [...] } from the last emit:

import { Subscriber } from '@pensarfeo/betterstore'

const bus = new Subscriber()
bus.observe((...args) => console.log(...args))
bus.emit(1, 2)
bus.observeOnce((x) => console.log('once', x))

| Method | Description | |--------|-------------| | observe(fn) | Returns an Observable. fn receives spread arguments from the last emit. Does not run until the next emit (same as Store.observe). | | observeOnce(fn) | Same as observe, but unsubscribes after the first notification. Returns the Observable so you can call unsubscribe() early if needed. | | emit(...args) | Sets internal state to { payload: args } and notifies observers; listeners receive ...args. |

Observer and Subscriber are interchangeable for many apps. Differences:

| | Observer | Subscriber | |--|------------|--------------| | One-shot API | once(fn) | observeOnce(fn) | | Internal payload | emit args stored as an array | { payload: args } object | | Optional name | stored on this.name (unused internally) | not used |

Prefer Observer for the shorter once name; use Subscriber when you want observeOnce naming aligned with observe, or when sharing the { payload } shape with other store-backed code.


Utility: partialSubscriber(keys, fn)

import { Store, partialSubscriber } from '@pensarfeo/betterstore'

const user = new Store({ name: 'A', age: 1, email: 'x' })
const sub = partialSubscriber(['name', 'age'], (obj) => console.log(obj))
user.observe(sub).run()

Callback receives the full latest object; it only fires when one of the listed keys changes (by !==).


Environment

The library sets env to:

  • maintypeof window !== 'undefined'
  • workerWorkerGlobalScope
  • node — otherwise

Persistence runs only in main. Multi-process runs when env !== 'node' and name is set.


Known limitations

| Area | Detail | |------|--------| | Store.subscribe | One-shot only (observerun → unsubscribe). Use observe(...).run() for ongoing listeners. | | Selectable | add() wires children via Toggleable.subscribe (one-shot). Selection does not stay in sync; tests in test/selectable.test.js are skipped. | | Selectable.bind / unsubscribe | Stores the return value of subscribe (an unsub function), not an Observable.bind() on those entries does not match the Observable API. | | Multi-process dispose | Not exposed on Store instances (see Multi-process above). | | Experimental sync | store/multiProcess/interface.js and worker.js are not part of the public Store API. |


Migration notes

Older docs or forks may use names that changed in 2.x:

| Was | Now | |-----|-----| | sub | observe (deferred) or subscribe (immediate + ongoing on writable / BasicStore) | | Subscribable | Observable | | Legacy Subscriber (pre-2.x bus) | Observer (emit / once) or Subscriber (emit / observeOnce) | | Derived with compute (a, b) => … | Derived aggregates source values into an array by index |

See MIGRATION.md for a short reference.


Example: Svelte

<script>
  import { Store } from '@pensarfeo/betterstore'

  const count = new Store(0)
  count.observe((v) => console.log(v)).bind()
</script>

<button onclick={() => count.value++}>
  {count.value}
</button>

License

ISC