1.0 compatibility contract
These are deliberate Valdres 1.0 contracts. They are useful when choosing the library, configuring a bundler, or deciding whether an upgrade changes application behavior.
ESM package and CommonJS loading
Valdres is ESM-only. The package does not ship a separate CommonJS build. Modern
Node.js can nevertheless load its ESM entry with require() through Node's
require(esm) support. The export map's default condition points to the same
ESM artifact as import, which is what a CommonJS resolver selects; it is not a
hidden CommonJS fallback. The published package requires Node.js 22.12 or newer.
Only the public export paths in the package export map are supported. Do not
import private dist chunks or source paths.
Size
The honest size for the core runtime is about 30 KB gzip in a minified
production consumer bundle. The exact number depends on the bundler and the
exports used; the release gate currently measures roughly 32 KB gzip for the
atom and atom + selector + store fixtures, and roughly 36 KB when every core
export is retained.
This is a consumer-bundle number, not the size of one source function after tree-shaking and not the compressed npm tarball. Valdres publishes minified JavaScript, and CI measures the packed artifact and representative consumer bundles on every relevant change.
Deep drop-equal is the default
Atoms, atom families, selectors, and selector families use deep structural equality by default. When the next settled value is deeply equal to the current value, Valdres drops the update: it keeps the current value and does not notify dependents or subscribers. This avoids rerenders when an immutable update recreates an equivalent object or collection.
Deep equality has a cost, and reference changes are sometimes meaningful. Use
the equal option as the explicit escape hatch. Object.is gives
reference/value identity semantics:
import { atom, selector } from "valdres"
const documentAtom = atom({ blocks: [] }, { equal: Object.is })
const viewSelector = selector(get => render(get(documentAtom)), {
equal: Object.is,
})
With equal: Object.is, replacing an object with a different but structurally
equal object is an update. In-place mutation remains unsupported because both
comparators see the same reference; replace the value when writing.
Process-less runtimes
When process.env.NODE_ENV exists, Valdres follows it. When process does not
exist, as in many edge, worker, and direct-CDN runtimes, the default package
entry behaves as production. It skips development-only deep-freezing, validation
diagnostics, warnings, and instrumentation.
For debugging a process-less application, enable the package's development
export condition. Apply the condition consistently to valdres and every
framework adapter so the app does not load isolated default and development
runtime graphs. A URL-based, no-build ESM application can request the published
file from a CDN, for example
https://unpkg.com/valdres@<version>/dist/development/index.js. This does not
make valdres/dist/development/index.js a supported package deep import: it is
not in the export map, so package consumers must use the development
condition. Bun enables that condition by default for development builds; use
bun build --production for a production process-less bundle.
Selector-family identity is stable while live
Calling a selectorFamily repeatedly with the same key returns the same member
while a caller or live store retains that member. A subscription, dependency
edge, or an enumerable store's cold cache can provide that retention. The family
cache is weak: after the member is no longer reachable, garbage collection may
reclaim it and a later call may create a new object identity.
This is the “stable while live” contract. It provides stable identity wherever
the reactive graph needs it without turning every key ever queried into a
permanent memory entry. Calling release(...args) evicts a future family lookup
but does not invalidate references or store entries that are already live.
Runtime and global-name singletons
Valdres permits one core runtime graph per JavaScript global. The first import
claims a globalThis instance slot; loading another copy throws, even when the
copies have the same version. This guard prevents split stores, registries, and
adapter commit coordination. It also means the default and development graphs
must not be mixed in one application.
Names on atoms and atom families are global addresses used by state transfer, not debug labels with module-local scope. They must be unique. Recreating an ordinary named atom or family throws instead of silently replacing its registered definition.
A named global atomFamily has intentional singleton-by-name behavior:
recreating it returns the original family, and the later default factory and
options do not replace the first definition. This also defines its HMR behavior.
During a hot update, the global family keeps the definition that was registered
first; perform a full page or process reload to apply edits to that definition.
Do not rely on HMR to migrate live global state or redefine a global family.
The instance guard and name registry live on the JavaScript global, so workers, iframes, and separate server processes each have their own instance and registry. They do not synchronize state across those boundaries.
Stability boundary
The exported declarations in the packed package's dist/types tree are the
public TypeScript API. Valdres freezes that surface at the first 1.0 release
candidate and checks subsequent changes semantically. Private source modules,
private distribution chunks, and undocumented internal object fields are not
part of the compatibility contract.