@azmr/core
Release history for @azmr/core.
View as Markdown for coding assistants, or see llms.txt.
0.1.1
Patch Changes
- ea59e9b: Package metadata:
homepagenow points at each package's page on platform.azmara.io (docs.azmara.io never resolved),bugslinks to the GitHub issues, and README doc links follow.@azmr/uigets a description that matches the design system it now is.
0.1.0
Minor Changes
-
de9fd88: Add
batch(fn), coalescing everyset()call made insidefninto a single effect flush, instead of one flush perset(). Values still update synchronously as usual —.get()/.peek()insidefnalways see the latest write; only the effect flush is deferred. Nests correctly: an innerbatch()completing does not trigger a flush while an outer one is still open.import { Signal, batch, effect } from "@azmr/core"; const first = new Signal("Aroha"); const last = new Signal("Ngata"); effect(() => console.log(`${first.get()} ${last.get()}`)); // → Aroha Ngata batch(() => { first.set("Tane"); last.set("Mahuta"); }); // → Tane Mahuta (logged once, not twice)Additive — no existing behaviour changes for callers not using
batch(). -
76435db:
Signal's constructor now accepts an optional{ equals }option, replacing the defaultObject.ischeckset()uses to decide whether a write is a no-op.import { Signal } from "@azmr/core"; const shallowEqual = (a: { x: number }, b: { x: number }) => a.x === b.x; const point = new Signal({ x: 1 }, { equals: shallowEqual }); point.set({ x: 1 }); // no-op — a different reference, but equal by equals()Useful for object/array values you want compared by shape rather than by reference, so setting a structurally-identical value doesn't notify subscribers or trigger a re-render.
Additive — no existing behaviour changes for callers not passing
equals;Object.isremains the default. -
c5724c2:
effect(fn)'s callback may now return a cleanup function. It runs right before the next re-run (after stale dependencies are dropped, before the callback runs again) and on dispose — the same shape as React'suseEffect.import { Signal, effect } from "@azmr/core"; const count = new Signal(0); const dispose = effect(() => { const id = setInterval(() => console.log(count.get()), 1000); return () => clearInterval(id); });The callback's return value is only treated as a cleanup when it's actually a function — existing effects that return other values (e.g.
effect(() => signal.get())) are unaffected.Additive — no existing behaviour changes for callers not returning a cleanup function.
-
a5b3307:
effect()'s disposer now actually detaches the effect from every signal it read, instead of being a no-op. Previously everyeffect()leaked its subscriber into everySignalit read, forever — and sincecomputed()callseffect()internally and discarded the disposer, every computed leaked too, with no way to dispose one at all.- The disposer removes the subscriber from every tracked signal and marks the effect disposed, so an in-flight flush skips it even if already queued — safe to call from inside another effect during the same flush.
- Stale dependencies are dropped before each re-run, so a conditional read
(
cond.get() ? a.get() : b.get()) that switches branches doesn't keep the effect subscribed to the untaken branch's signal. - New:
computed()'s return value now carries a non-enumerabledispose()that stops recomputation and detaches from every signalfnreads — the return type isSignal<T> & { dispose(): void }, additive over the previousSignal<T>.
Non-breaking for existing callers —
effect()'s signature is unchanged andcomputed()'s return value is still a fully-functionalSignal<T>, just with one new property.@azmr/ui'suseSignaland any other long-livedeffect()/computed()consumer now actually stops leaking on unmount once its disposer is called. -
d644047: Add
onError(handler), so an exception thrown by aneffect()re-run or asubscribe()callback during a flush no longer propagates out of an unrelated caller'sset()and aborts every other subscriber still pending in that flush.import { Signal, effect, onError } from "@azmr/core"; const restore = onError((error) => { console.error("effect failed:", error); }); const count = new Signal(0); effect(() => { if (count.get() > 10) throw new Error("too high"); });onErrorreturns a function that restores whichever handler was active before the call. Without a registered handler, the error is rethrown asynchronously instead of being silently swallowed, without blocking the flush that surfaced it.Doesn't apply to
effect(fn)'s first, synchronous run, or to a directfn()call insidebatch()/untrack()— those still throw normally to the caller, who is right there to catch them.Additive — no existing behaviour changes for callers not using
onError(). -
5e05e8f:
computed()is now lazy and pull-based instead of eager.fnno longer runs at creation, and while a computed is unobserved (nothing reading it viaeffect(), anothercomputed(), orsubscribe()), an upstream change only marks it stale — it doesn't recompute. The real computation happens on the next.get()/.peek(), always from the latest values. Once something does depend on it, it switches to recomputing eagerly on every upstream change, matching the old always-eager behaviour, so an observing effect still only re-runs when the derived value itself actually changes.const price = new Signal(100); const doubled = computed(() => price.get() * 2); // fn hasn't run yet price.set(200); // doubled is unobserved — just marked stale, not recomputed price.set(300); doubled.get(); // 600 — computes once here, from the latest valueBehaviour change / migration note: a throwing
fnused to throw synchronously fromcomputed(fn)itself. It now throws from the first.get()/.peek()instead, since that's whenfnactually runs. If you relied oncomputed()throwing at creation time (e.g. to fail fast during setup), wrap the first read instead:const doubled = computed(fn); doubled.get(); // any error from fn now surfaces here, not at computed(fn)No other observable change for the common case of an effect (or another computed) reading a computed's value — the eager recompute-on-write path is unchanged while observed.
-
7eb9c43: Add
untrack(fn), runningfnwith dependency tracking suspended so any.get()calls made inside it — including ones buried in codefncalls into, not just a direct call at the top level — don't register a dependency on the currently-running effect. Unlike.peek(), which only works where you control the call site,untrack()composes: it works even whenfncalls third-party or generic code you can't rewrite to use.peek()internally.import { Signal, effect, untrack } from "@azmr/core"; const count = new Signal(0); const debugFlag = new Signal(false); effect(() => { if (untrack(() => debugFlag.get())) console.log(`count: ${count.get()}`); });A no-op outside an effect. An effect created inside
fnstill tracks its own reads normally — the suspension only applies to the effect that was active whenuntrack()was called.Additive — no existing behaviour changes for callers not using
untrack().
Patch Changes
-
a5a2134: Fix
exportscondition order (typesbeforeimport) and declare"sideEffects": falseon@azmr/core,@azmr/ui, and@azmr/security(both the.and./browserconditions onsecurity).The
types-first order previously worked only because TypeScript falls back to the siblingindex.d.tswhen the condition order doesn't matter to it — bundlers with stricterexportsresolution (e.g. Next.js's Turbopack/webpack) are not guaranteed to be as forgiving.sideEffects: falselets bundlers tree-shake unused named exports out of consuming bundles; verified with an esbuild bundle of a single named import from@azmr/security/browser— no unrelated exports leak in.No public API change.
0.0.2
Patch Changes
-
5bf56bb: Fix package metadata: mojibake in the description field (encoding mismatch, was showing garbled text on npm) and a missing LICENSE file in the published tarball despite being listed in
files.@azmr/aigot the same fixes plus missinghomepage/repository/author/keywords, but isn't included in this changeset - it's on changesets' ignore list and doesn't take version bumps this way. -
d70fccb: Upgrade workspace dependencies across the monorepo (pnpm 10 -> 11 tooling migration): React 18.3 -> 19.2 (
@azmr/ui), Zod 3.24 -> 4.4 (@azmr/security), better-sqlite3 11.7 -> 12.11 (@azmr/db), plus vitest, tsup, and@types/nodebumped to latest across all packages.typescriptstays pinned at^5.7.3(TypeScript 7 currently breakstsup's.d.tsbundling viarollup-plugin-dts).
All notable changes to this project will be documented in this file.
[0.0.1]
Added
- Initial release of reactive engine with signals, effects, and computed values
- Built-in infinite-loop protection
- Automatic UI updates when data changes