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

@caspian-vega/nano-macro-contracts

v0.4.1

Published

Lightweight, opinionated contracts built on top of nanostores: defineStore (chain-composed macro-stores with state, async resources, computes and actions) and a small set of RxJS-like store operators.

Readme

@caspian-vega/nano-macro-contracts

Store contracts on top of nanostores. defineStore composes state, async resources, derived values, actions and effects into one class where every member is a real nanostore.

Part of caspian-vega-astro-libs.

Install

npm install @caspian-vega/nano-macro-contracts nanostores @nanostores/async @nanostores/logger
# or
pnpm add @caspian-vega/nano-macro-contracts nanostores @nanostores/async @nanostores/logger

nanostores, @nanostores/async and @nanostores/logger are peer dependencies.

Usage

import { browserOnly, debounced_, defineStore } from '@caspian-vega/nano-macro-contracts/macro-store';
import { logger } from '@nanostores/logger';
import { computed } from 'nanostores';

class PostStore extends defineStore('PostStore', { query: '', topicId: '' })
    .resource('posts', {
        pipe: browserOnly(debounced_(300)),
        load: async ({ query, topicId }) => fetchPosts({ query, topicId }),
        initial: [] as Post[],
    })
    .derive((s) => ({
        postCount: computed(s.posts.value, (posts) => posts.length),
    }))
    .actions((s) => ({
        setQuery: (query: string) => s.patch({ query }, 'setQuery'),
    }))
    .logger(import.meta.env.DEV ? logger : undefined)
    .build() {}

const store = new PostStore();

await store.posts.ready();
store.posts.value.get();
store.setQuery('astro');

Each step returns a newly typed builder, so the store argument of every later callback is inferred from what was registered before it.

Entry points

| Import | Contains | | ------------------- | ------------------------------------------------------------------------------------------------- | | . | Everything below | | ./macro-store | defineStore, browserOnly, createResource, assertStoreDefinition, RESERVED_STORE_MEMBERS | | ./nanostore-utils | pipeLine and the operators | | ./nanostore-ready | whenReady | | ./nanostore-lite | NanoContract, NanoPromise |

Chain order

flowchart LR
  D["defineStore(name, initial)"] --> F["from(source)"]
  D --> S["standalone()"]
  D --> R["resource(name, def)"]
  F --> R
  S --> R
  R --> P["deps(factory)"]
  D --> P
  P --> V["derive(factory)"]
  V --> A["actions(factory)"]
  A --> E["effect(factory)"]
  E --> L["logger(fn, options)"]
  L --> X["extend(factory)"]
  X --> B["build()"]

Steps may repeat and may be ordered freely. A factory only sees members and dependencies declared above it.

API

defineStore(name, initialState?)

Starts a chain.

  • name (string, required): shown in logger output and in collision errors.
  • initialState (object, optional): the state shape. Constrained to object, so an interface is accepted with satisfies, with as, or as an annotated constant.
  • Returns: StoreBuilder, carrying state, snapshot, patch, setState, reset and destroy.
defineStore('ProductStore', { q: '', category: null } satisfies ProductFilterState);

Chain steps

| Step | Adds | | ----------------------------------- | --------------------------------------------------------- | | .resource(name, def) | a Resource<TData> driven by the store's state | | .from(source).resource(name, def) | a resource driven by a narrower projection | | .standalone().resource(name, def) | a resource driven by nothing, refreshed by reload() | | .deps(factory) | private dependencies, not members | | .derive(factory) | derived nanostores | | .actions(factory) | methods placed on the instance | | .effect(factory) | a side effect, a returned function becomes cleanup | | .logger(fn, options?) | binds state to a logger and enables patch(..., label) | | .extend(factory) | non-store, non-function members | | .build() | the store class |

Extend the built class to get a named, DI-friendly type:

class PostStore extends defineStore('PostStore', {}).build() {}

Base members

| Member | Signature | Produces | | ---------------------- | ------------------------------------------------------------- | ------------------------------------------------ | | state | MapStore<TState> | the nanostore, usable with computed(...) | | snapshot | TState | synchronous read of the current value | | patch(patch, label?) | Partial<TState> \| ((s: TState) => Partial<TState>), string | merges a partial, label names the action | | setState(next) | TState | replaces the whole state | | reset() | | restores a shallow copy of the initial state | | destroy() | | runs cleanups and unbinds the logger, idempotent |

.resource(name, def)

Adds an async member driven by the store's state.

  • name (string, required): member name.
  • def.load ((source, store, deps) => Promise<TData>, required): source is the state, or the .from projection.
  • def.initial (TData, required): value of resource.value before the first load.
  • def.pipe (StoreOperator<TSource>[], optional): operators applied to the source.
  • def.enabled ((source) => boolean, optional): return false to skip the load and keep the current value.
  • Returns: the builder, with Resource<TData> under name.
.resource('posts', {
    pipe: browserOnly(debounced_(300)),
    enabled: ({ query }) => query.length > 2,
    load: ({ query }) => api.searchPosts(query),
    initial: [] as Post[],
})

Resources are lazy. The loader runs when something first subscribes, or when ready() is awaited, not when the store is constructed. value keeps the last successful value while a reload is in flight or has failed.

The source is the whole state map, so every patch(...) re-runs the loader. For a parameter-free loader such as getAllGreetings(), use .standalone() instead.

.from(source)

Narrows what the next resource reloads on.

  • source ((store) => ReadableAtom<TSource>, required)
  • Returns: ScopedStoreBuilder, whose .resource(...) receives TSource instead of the state.
defineStore('TopicStore', { topicId: '', unrelatedFilter: '' })
    .from((s) => computed(s.state, ({ topicId }) => topicId))
    .resource('topic', {
        load: (topicId) => fetchTopic(topicId),
        initial: null,
    });

.standalone()

Detaches the next resource from the state entirely, for a loader that takes no input.

  • Takes no arguments.
  • Returns: StandaloneStoreBuilder, whose .resource(...) takes a StandaloneResourceDef.
const GreetingStore = defineStore('GreetingStore', { filter: '' })
    .deps(() => ({ api: svInject(GreetingApi) }))
    .standalone()
    .resource('greetings', {
        load: (_store, { api }) => api.getAllGreetings(),
        initial: [] as Greeting[],
    })
    .build();

const store = new GreetingStore();

await store.greetings.ready(); // first load
store.greetings.reload(); // every later refresh

The loader drops the dead source argument: it receives the store and the deps bag directly. pipe and enabled are not offered, having nothing to act on.

  • Without .standalone(), the source is the whole state map, so every patch(...) re-runs the loader even when it reads nothing from state. This is the reason the step exists.
  • The resource stays lazy. It loads when something first subscribes or ready() is awaited, never at construction.
  • reload() before the first subscription does not load early. The pending trigger folds into the first load.
  • While a reload is in flight, loading is true and value still holds the previous result.
  • A failed load leaves error set and value untouched. The next reload() clears the error on success.
  • The constant source is created per instance, so two instances of the same store reload independently.

Standalone and state-reactive resources mix freely in one chain:

defineStore('CatalogStore', { id: '' })
    .standalone()
    .resource('catalog', { load: (_s, d) => d.api.getCatalog(), initial: [] })
    .from((s) => computed(s.state, ({ id }) => id))
    .resource('item', { load: (id, _s, d) => d.api.getItem(id), initial: null });

.deps(factory)

Registers collaborators the store uses but does not expose.

  • factory ((deps) => object, required): runs once per instance, at construction.
  • Returns: the builder. Every later factory receives the bag as its second argument, a resource loader as its third.
class GreetStore extends defineStore('GreetStore', { greeting: 'hello' })
    .deps(() => ({ userService: svInject(UserService) }))
    .actions((s, d) => ({ greet: () => d.userService.sendGreeting(s.snapshot.greeting) }))
    .build() {}

const store = new GreetStore();

store.greet();
Object.keys(store); // ['greet']
  • Dependencies are not members. The bag lives in a module-private WeakMap, so it is absent from Object.keys, spreads, JSON.stringify and snapshot.
  • Own namespace, so a dependency may share a name with a member. Only a dependency declared twice collides.
  • Several .deps(...) steps accumulate. Each bag is frozen.
  • Dependencies are borrowed, not owned. There is no teardown hook. Allocate in .effect(...) instead.

.derive(factory)

  • factory ((store, deps) => Record<string, ReadableAtom<any>>, required)
  • Returns: the builder, with each returned atom as a member.
.derive((s) => ({ total: computed(s.state, ({ items }) => sum(items)) }))

.actions(factory)

  • factory ((store, deps) => Record<string, Function>, required)
  • Returns: the builder, with each returned function as an instance method.
.actions((s) => ({ add: (item: Item) => s.patch({ items: [...s.snapshot.items, item] }, 'add') }))

.effect(factory)

  • factory ((store, deps) => void | (() => void), required): a returned function is registered as cleanup and runs on destroy().
  • Returns: the builder.
.effect((s) => {
    const off = s.state.listen(persist);
    return off;
})

.logger(fn, options?)

Binds state to @nanostores/logger, or any drop-in with the same signature.

  • fn (StoreLoggerFn | undefined, required): undefined is a no-op.
  • options (LoggerOptions, optional): defaults suppress mount and unmount noise.
  • Returns: the builder. Once bound, the label argument of patch(...) appears as a named action.
.logger(import.meta.env.DEV ? logger : undefined)

.extend(factory)

Escape hatch for public members that are neither stores nor functions.

  • factory ((store, deps) => object, required)
  • Returns: the builder.
.extend(() => ({ formatter: new Intl.NumberFormat() }))

.build()

  • Returns: StoreConstructor<TShape>, a class. It runs no factories. Every factory runs once per instance, at construction.

assertStoreDefinition(Store)

Constructs the class once and destroys it, so collisions surface in a unit test instead of in a request.

  • Store (new () => StoreLifecycle, required)
  • Returns: void
  • Throws: Error listing every collision in the definition.
it('is a valid store definition', () => {
    expect(() => assertStoreDefinition(PostStore)).not.toThrow();
});

browserOnly(...operators)

  • operators (StoreOperator<T>[], required)
  • Returns: the operators on the client, an empty array on the server. Keeps timers out of SSR.
pipe: browserOnly(debounced_(300));

createResource(store, source, def, deps)

Low-level constructor behind .resource(...). Use the chain step unless a resource is needed outside a store.

  • Returns: Resource<TData, TError>

RESERVED_STORE_MEMBERS

ReadonlySet<string> holding state, snapshot, patch, setState, reset, destroy.

pipeLine(store, ...operators)

Applies operators left to right.

  • store (ReadableAtom<T> | WritableAtom<T>, required)
  • operators (StoreOperator<T>[], required)
  • Returns: the same atom kind that went in.
const query = pipeLine(
    input,
    debounced_(250),
    distinct_(),
    sideEffect_((v) => console.log(v))
);

Operators

| Operator | Curried form | Produces | | -------------------------------------- | -------------------- | ----------------------------------------- | | debounced(store, delay = 300) | debounced_(delay) | emits after the value stops changing | | throttled(store, delay = 300) | throttled_(delay) | emits at most once per interval | | delayed(store, delay = 300) | delayed_(delay) | emits every value, late | | filter(store, predicate) | filter_(predicate) | emits only values passing the predicate | | distinct(store, compare = Object.is) | distinct_(compare) | drops consecutive equal values | | sideEffect(store, fn) | sideEffect_(fn) | runs fn per value and passes it through |

All return WritableAtom<T>. Subscriptions and timers are allocated only while the derived atom has listeners.

whenReady(stores, predicate, timeout?)

Waits until one or more stores satisfy a predicate.

  • stores (ReadableNanoStore<T> or an array of them, required): anything with get and listen.
  • predicate ((...values) => boolean, required)
  • timeout (number, default 3000): milliseconds.
  • Returns: Promise<WhenReadyResult> carrying ready, timedOut and values. Never rejects.
await whenReady(store.posts.value, (posts) => posts.length > 0);
await whenReady([userAtom, settingsAtom], (user, settings) => !!user && !!settings, 5000);

Subscribing mounts a lazy store and triggers its work. The subscription is released once the gate settles.

Types

Resource

  • async (ReadableAtom<AsyncValue<TData>>): raw lifecycle.
  • value (ReadableAtom<TData>): last good value, seeded by initial.
  • loading (ReadableAtom<boolean>)
  • error (ReadableAtom<TError | undefined>)
  • reload(): void
  • ready(timeout?): Promise<ResourceReadyResult<TData, TError>>: never rejects.

ResourceReadyResult

  • value (TData, optional)
  • error (TError, optional)
  • timedOut (boolean)

ResourceDef

Definition object accepted by .resource(name, def). Fields are listed under .resource above.

StandaloneResourceDef

Definition object accepted by .standalone().resource(name, def).

  • load ((store, deps) => Promise<TData>, required)
  • initial (TData, required)

StoreOperator

(store: ReadableAtom<T>) => WritableAtom<T>

WhenReadyResult

  • ready (boolean)
  • timedOut (boolean)
  • values (readonly tuple of the store values)

MacroStoreDestroyable

  • onMacroStoreDestroyed(): void

Optional teardown hook for a subclass that owns resources the chain never saw. It is looked up on the instance at teardown and runs before the chain cleanups, so derived atoms, resources and actions are still usable inside it. If it throws, the chain cleanups still run and the error propagates out of destroy().

class SocketStore extends defineStore('SocketStore', { url: '' }).build() implements MacroStoreDestroyable {
    private readonly socket = new WebSocket(this.snapshot.url);

    onMacroStoreDestroyed(): void {
        this.socket.close();
    }
}

NanoContract

WritableAtom<T> | PreinitializedWritableAtom<T>, a writable store contract.

NanoPromise

ReadableAtom<AsyncValue<T>>, a read-only async value.

ReadableNanoStore

Minimal shape accepted by whenReady: get() plus listen(cb).

Others

StoreBuilder, ScopedStoreBuilder, StoreConstructor, StateApi, StoreLifecycle, StoreLoggerFn, StoreState, StoreDeps, AnyAtoms, AnyActions, StoreMemberConflict, NoOverlap, FreeName, NoDepOverlap, StoreDepConflict. These support inference in the chain and are rarely written by hand.

Collisions

Member names share one flat namespace. A collision, against a reserved name or against a member an earlier step defined, is caught twice.

  1. At compile time, on the offending key:
.actions(() => ({ reset: () => {} }))
// error: Type '() => void' is not assignable to type
//        '(() => void) & StoreMemberConflict<"store member \"reset\" is already defined">'
  1. At the first construction, with the store name, the chain step, and every collision in one error:
[InquiryStore] .actions() cannot define "reset": it is a reserved store member
  (state, snapshot, patch, setState, reset, destroy).
  Rename it, or call the built-in store.reset().

Only the six reserved names are taken. Object.prototype members such as toString are ordinary member names.

Constraints

  • Factories run once per instance, at construction. build() runs none of them, so a collision surfaces at first construction. Use assertStoreDefinition(...) in a test to force it in CI.
  • A factory that throws fails construction and the original error propagates. Collisions already collected are reported instead, with the throw attached as cause. The half-built instance is destroyed either way.
  • Allocating in .extend(...) or .derive(...) leaks unless teardown is handed to destroy():
// leaks
.extend(() => ({ conn: openConn() }))

// hands over teardown
.extend(() => {
    const conn = openConn();
    return { conn, onMacroStoreDestroyed: () => conn.close() };
})

// inert holder, effect acquires and releases
.extend(() => ({ conn: lazyConn() }))
.effect((s) => { s.conn.open(); return () => s.conn.close(); })
  • An .extend(...) hook shadows a subclass hook. The extend factory defines an own property during super(), a subclass method lives on the prototype, so the own property wins and no collision is reported. Use one or the other, never both.
  • reset() restores a shallow copy of the initial state, so nested objects keep their original references. Pass a fresh structuredClone(INITIAL) per store when state holds mutated objects.
  • Members are non-writable but configurable, so vi.spyOn(store, 'submit') works.

Synergies

See SYNERGIES.md.

License

MIT