Changelog
Unreleased
Fixed
- Watcher leak / wrong-removal in
atom.watch(key, cb). The unsubscribe captured an index from the subscription site, and that index drifted as earlier callbacks unsubscribed — splicing the wrong (or out-of-range) callback. Unsubscribe is now identity-based: it removes the callback by reference. - Watcher diff used the raw
newValueargument. When callers passed an updater function toupdate(...), the watcher comparison ranget(newValue, key)on the function itself, so every watcher fired (or none did) regardless of what changed. Now uses the resolvedupdatedValue. reset()returned dead code. The function assignedconst update = this.update(...)(returnsvoid) and thenreturn update. The local was alwaysundefined. Removed.reset()didn’t deep-clone the default. After the firstupdatethat mutatedcurrentValue, subsequentreset()calls handed callers a reference into shared default state. Now clones via@mongez/reinforcements’clone.clone()collisions on heavy use. The 4-digitRandom.int(1000, 9999)suffix collided in birthday-paradox territory after a few thousand clones. Replaced with a monotonic counter (.clone.1,.clone.2, …).get(key)auto-bound any function-valued property. Every function has a.bind, so the implementation rebound every function returned fromget()tothis.currentValue. That broke pre-bound methods and generated a new function identity per read. Removed entirely.change/silentChangeon primitive atoms silently corrupted state. Callingatom.change("foo", "bar")on abooleanatom spread the primitive ({...true, foo: "bar"}→{foo: "bar"}) and replaced the boolean with an object. Now a compile error at the type level (see “Changed”).- Getter-based actions crashed
createAtom.Object.keys(actions).forEach(k => actions[k].bind(atom))invoked any getter eagerly; if the getter referencedthis.value(which wasn’t on the atom yet) it returnedundefined, and.bindblew up. The action installer now usesObject.getOwnPropertyDescriptorand forwards getters as getters.
Added
derive(key, get => …)— derived atoms with auto-tracked dependencies. The compute function runs eagerly on creation and on every dependency change. Dynamic dependency graphs are supported (conditional reads add/drop deps across runs). Errors insidecomputeare surfaced asynchronously without breaking the source atom’s update cycle.persist: true | PersistAdapter— atom-level persistence.trueuses the built-in localStorage adapter (no-ops on the server); aPersistAdapterobject lets you plug in cookies, IndexedDB,@mongez/cache, etc. The adapter is called for the initial read (sync or async), every update writes through, andreset()removes the entry. Sync errors and async rejections from the adapter are swallowed — a transient storage error (quota, private-mode block) never crashes the atom.AtomStore+createAtomStore. Per-request isolation primitive for SSR. Each store holds scoped clones of atom templates, exposesuse(template),get(key),hydrate(snapshot),snapshot(), anddestroy(). The React-side glue (AtomStoreProvider,useAtom,useAtomStore) lives in@mongez/react-atom.clone({ register: false })option. Lets the store create scoped clones without polluting the module-levelatomsregistry.enableAtomDevtools(options?). Redux DevTools bridge with initial-snapshot, per-update timeline entries,JUMP_TO_STATE/JUMP_TO_ACTIONtime-travel viasilentUpdate, ignore-patterns, configurable scan interval for late-registered atoms.- AI kit.
llms.txt,llms-full.txt, andskills/folder (README,overview,atoms,collections,stores,actions,devtools,recipes) for tool-assisted development. - Test suite. 52 unit tests across
atom,atom-collection,atom-store, anddevtools. - CI. GitHub Actions workflow: Node 18/20/22 × Ubuntu, plus Node 20 × Windows.
Changed (breaking)
Atom<V, A>is now a conditional type. Object-only methods (merge,change,silentChange,get(key),watch(key, cb)) are removed from the type whenVis a primitive.Atom<boolean>.change(...)is a compile error.Atom<any>keeps both surfaces (legacy permissive default).AtomActions<V>no longer includes| any. Was[key: string]: (...) => any | any. The| anycollapsed the entire type toanyand defeated per-action type safety. Now just(this: Atom<V>, ...args: any[]) => any.AtomOptions.defaultisV, notV | Partial<V>.Partial<V>accidentally allowed incomplete defaults that the type didn’t reflect at runtime.change/silentChangesignatures are(key: T, newValue: V[T]), not(key, newValue: any). The wideranydefeated type safety for known-shape atoms.clone()accepts{ register?: boolean }. Pure addition for stores; existing callers (atom.clone()) keep working.
Removed
- Auto-
bindinatom.get(key). Functions returned fromget()are no longer rebound tocurrentValue. If your code relied on this, wrap the call:atom.get("fn")?.bind(atom.value). - Random-suffix clone keys (
...Cloned9123). Replaced by deterministic counter (...clone.1,...clone.2).
Dependency bumps
@mongez/reinforcements: ^2.3.10→^3.1.0. Compatible API for the surfaces atom uses (clone,get). See reinforcements v3 MIGRATION for the full diff.
Tests
46 + 6 devtools + 7 derive + 12 persist = 71 passing