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

@avinlab/form

v0.5.0

Published

Stable form controller with readonly snapshots and selective subscriptions.

Readme

@avinlab/form

A form is one stable mutable controller for its lifetime. values and prevValues are replaceable readonly snapshots; calling a mutation method changes snapshots without replacing the controller. Read snapshots imperatively and mutate only through the controller.

Install

npm install @avinlab/form

Advanced controller contract

  • setValue and full-replacement setValues compare every top-level field with Object.is. NaN is therefore equal to NaN, while 0 and -0 are different. Objects, arrays, dates, maps, and sets change only when their field reference changes.
  • A no-op preserves both snapshot references and sends no notification. After a successful commit, values is the new snapshot and prevValues is the snapshot immediately before that commit.
  • Changed-field listeners run once each before whole-form listeners. Changed fields follow the key order of the next snapshot; removed fields follow their previous key order.
  • An update requested by a listener is queued until the current notification cycle finishes. Reentrant updates run FIFO and every callback receives the current and previous snapshots for its own commit.
  • Every listener captured for a notification is attempted. One failure is rethrown after dispatch; multiple failures are reported in one AggregateError. A listener failure does not roll back the committed snapshot.
  • subscribe and subscribeField use set semantics and return idempotent cleanup functions. Adding or removing listeners during dispatch affects the next commit, not the captured current cycle.
  • Snapshots are shallow-frozen in development and readonly in TypeScript. Nested field values are not cloned or deeply frozen: replace a nested value through setValue instead of mutating it.

Controller recipe

import { createForm } from '@avinlab/form';

const form = createForm({ name: 'Bob', age: 20 });
const unsubscribeAge = form.subscribeField('age', (age, previousAge) => {
  console.log({ age, previousAge });
});
const unsubscribeForm = form.subscribe((values, previousValues) => {
  console.log({ values, previousValues });
});

form.setValue('age', 21);
form.setValues({ name: 'Ada', age: 36 }); // full snapshot replacement

unsubscribeAge();
unsubscribeForm();

Keeping form in a dependency list or passing it through application layers is safe: successful commits retain the exact same controller object.

Synchronous validation

createFormValidation derives one readonly validation result from the form. A validator runs synchronously when configured, after each successful form commit, when replaced with a different validator, or when validate() is called. It receives the committed values and prevValues. The returned object is copied, top-level undefined entries are omitted, and the caller's object is never mutated. status is unvalidated before any validator completes, valid for an empty normalized result, and invalid otherwise. Subscribers receive the complete { status, errors } result. Complete results are compared by deep structural equality: independently created nested objects and arrays with the same contents preserve the existing result reference and do not notify. Error structures must be acyclic.

import { createForm, createFormValidation } from '@avinlab/form';

interface Values {
  name: string;
  age: number;
}

type Errors = Partial<Record<keyof Values, string>>;
const form = createForm<Values>({ name: 'Bob', age: 20 });
const validation = createFormValidation<Errors, Values>(form, (values) => ({
  name: values.name ? undefined : 'Name is required',
  age: values.age >= 18 ? undefined : 'Must be at least 18',
}));

const unsubscribe = validation.subscribe((result) => console.log(result));
form.setValue('age', 15); // validation completes before setValue returns

validation.setValidator((values) => ({
  age: values.age >= 21 ? undefined : 'Must be at least 21',
})); // recalculates the current snapshot synchronously

unsubscribe();
validation.dispose(); // idempotent; releases the form subscription and listeners

Without a validator, result is { status: 'unvalidated', errors: {} }; both the result and its empty errors object are readonly. Only result.status === 'valid' means validation completed successfully. If a validator throws, the controller publishes that empty unvalidated result before propagating the exception. Disposal is explicit and idempotent; a disposed validation controller retains its final result and no longer reacts to form commits.

Strictly compiled controller and validation recipes live in examples/react/src/examples.

Advanced validation lifecycle

Core validation subscribes to the supplied form until dispose() is called. setValidator recalculates synchronously, validate() explicitly repeats the current validator, and deeply equivalent complete results preserve their reference without notifying. A validator exception first publishes the shared empty unvalidated result and then rethrows. Subscription cleanup and disposal are idempotent; after disposal, validation retains its final result and ignores validation, validator, subscription, and form-update requests.