Skip to content

Releases: sathvikc/lume-js

v2.0.0

06 May 03:18

Choose a tag to compare

Lume.js v2.0.0 Release Notes

Release Date: May 5, 2026

🎉 What's New

Lume.js v2.0.0 stable is a major evolution from v1.0.0. The core APIs (state(), effect(), bindDom()) remain unchanged in signature, but the internals are completely rearchitected for security and extensibility. New capabilities include an extensible DOM handler system, a plugin wrapper, explicit effect dependencies, minified CDN bundles, and a suite of new addons.


🏆 Highlights

  • 319 tests | 100% funcs, 99.61% stmts/lines, 99.29% branches across 15 source files (+205 from v1.0.0)
  • Core bundle: 2.45KB gzipped / 3KB budget (81.5%)
  • Extensible handler system: Add custom data-* attribute behaviors via lume-js/handlers
  • Plugin system via withPlugins(): Intercept reads, writes, and notifications without touching core
  • repeat() stabilized: Keyed list rendering promoted from @experimental to fully supported
  • Security hardening: Eliminated globalThis.__LUME_CURRENT_EFFECT__ spoofing vector
  • Minified CDN bundles: dist/index.min.mjs, dist/addons.min.mjs, dist/handlers.min.mjs
  • Console-less environment safety: Works in service workers and embedded engines

📦 Key Features

1. Extensible Handler System (NEW in v2)

Custom data-* attribute handlers for DOM binding, shipped as lume-js/handlers.

import { bindDom } from 'lume-js';
import { show, classToggle } from 'lume-js/handlers';

bindDom(document.body, store, {
  handlers: [show, classToggle('active')]
});

Built-in handlers: data-hidden, data-disabled, data-checked, data-required, data-aria-expanded, data-aria-hidden. User handlers override built-ins with the same attribute name. lume-js/handlers did not exist in v1.0.0.

2. Plugin System via withPlugins() (NEW in v2)

Intercept reads, writes, subscriptions, and notifications without modifying core.

import { state } from 'lume-js';
import { withPlugins, createDebugPlugin } from 'lume-js/addons';

const store = withPlugins(state({ count: 0 }), [
  createDebugPlugin({ label: 'counter' })
]);

Plugin hooks: onInit, onGet, onSet, onNotify, onSubscribe. Full error isolation per hook. withPlugins() returns a new proxy — core state() is untouched. Includes $dispose() for cleanup in long-lived SPAs.

3. effect() with Explicit Dependencies (NEW in v2)

effect(fn) auto-tracking existed in v1.0.0. v2 adds an explicit dependency overload for side-effects that should not auto-track all reads.

import { effect } from 'lume-js';

// Auto-tracking (unchanged from v1):
effect(() => {
  console.log(store.count); // tracks 'count' automatically
});

// Explicit dependencies (NEW in v2):
effect(() => {
  analytics.track('count', store.count);
}, [[store, 'count']]);

4. New Addons (NEW in v2)

  • withPlugins() — intercept reads, writes, subscriptions, and notifications without modifying core. Already covered in section 2 above.
  • createCleanupGroup() — collect cleanup/unsubscribe functions, dispose all at once. Ideal for route changes or component unmount.
  • hydrateState() — reads initial state from <script type="application/json"> in server-rendered HTML. Zero-config SSR hydration.
  • debug addon — log all reactive operations with configurable output.

5. repeat() Promoted to Stable (IMPROVED in v2)

repeat() existed in v1.0.0 as @experimental. In v2.0.0 it is fully supported with 59 tests and zero open correctness issues.

import { repeat } from 'lume-js/addons';

repeat(listContainer, store, 'items', {
  render: (item) => `<li>${item.name}</li>`,
  key: (item) => item.id
});

Refactored in v2: reconcileDOM and applyPreservation helpers extracted from inline code for maintainability. Focus/scroll preservation and lifecycle hooks (onInsert, onRemove, onMove) existed in v1.0.0.

6. Minified CDN Bundles (NEW in v2)

Terser-generated *.min.mjs for direct <script type="module"> CDN usage.

<script type="module">
  import { state, bindDom, effect } from 'https://unpkg.com/lume-js@2.0.0';
  import { withPlugins, repeat } from 'https://unpkg.com/lume-js@2.0.0/addons';
</script>

7. Security & Architecture Hardening (IMPROVED in v2)

  • Replaced globalThis.__LUME_CURRENT_EFFECT__ with module-scoped currentEffect + getCurrentEffect() export. Third-party scripts can no longer spoof dependency tracking.
  • Scope-based read tracking via withReadObserver: state.js has zero knowledge of effect.js when no effect is running. Multiple simultaneous observers supported via a Set of active readers.
  • Console-less safety: src/utils/log.js ensures the library works in service workers and embedded engines without crashing on missing console.

📊 Stats & Improvements

Test Coverage

  • 319 tests | 100% funcs, 99.61% stmts/lines, 99.29% branches across 14 source files
  • +205 tests since v1.0.0 (114 → 319)
  • New test suites: handlers, integration, withPlugins, debug, cleanupGroup, hydrateState

Bundle Size

  • Core: 2.45KB gzipped (81.5% of 3KB budget)
  • Core files:
    • core/bindDom.js — 1.04KB
    • core/effect.js — 0.48KB
    • core/state.js — 0.92KB

Performance (vs v1.0.0)

  • Unsubscribe: indexOf + splice replaces .filter() — O(n) without allocation
  • Flush loop: Stable-index while loops replace array spreads — zero per-flush allocations
  • withPlugins flush: Single microtask via $beforeFlush hook
  • Effect cleanup: splice(0) replaces [...cleanups] spread
  • Console-less environments: No runtime errors in service workers

📚 Resources


⚙️ Installation

npm install lume-js
<!-- CDN -->
<script type="module">
  import { state, bindDom, effect } from 'https://unpkg.com/lume-js@2.0.0';
  import { withPlugins, repeat } from 'https://unpkg.com/lume-js@2.0.0/addons';
</script>

🚀 Migration Guide

From v1.0.0 to v2.0.0

Breaking Changes

  1. isReactive() import path changed

    // v1.0.0:
    import { isReactive } from 'lume-js';
    
    // v2.0.0:
    import { isReactive } from 'lume-js/addons';
  2. Package imports from dist/ (pre-built)
    v1.0.0 shipped raw source files as main. v2.0.0 ships pre-built dist/index.mjs.
    Bundlers and direct imports work identically; CDN URLs should use the new path if referencing files directly.

New Features You Can Adopt

  1. Custom DOM handlers:

    import { bindDom } from 'lume-js';
    import { show } from 'lume-js/handlers';
    bindDom(root, store, { handlers: [show] });
  2. Plugins via withPlugins():

    import { withPlugins } from 'lume-js/addons';
    const store = withPlugins(state({ count: 0 }), [myPlugin]);
  3. Explicit effect dependencies (for side-effects that should not auto-track):

    effect(() => {
      analytics.track('count', store.count);
    }, [[store, 'count']]);
  4. createCleanupGroup() — batch dispose:

    import { createCleanupGroup } from 'lume-js/addons';
    const group = createCleanupGroup();
    group.add(effect(() => { ... }));
    group.add(bindDom(root, store));
    group.dispose();
  5. hydrateState() — SSR hydration:

    import { hydrateState } from 'lume-js/addons';
    const store = hydrateState('server-state');
  6. Minified CDN bundles:

    <script type="module">
      import { state } from 'https://unpkg.com/lume-js@2.0.0/dist/index.min.mjs';
    </script>

💬 Getting Help

Full Changelog: v1.0.0...v2.0.0

v2.0.0-beta.3

06 May 02:30

Choose a tag to compare

v2.0.0-beta.3 Pre-release
Pre-release

Lume.js v2.0.0-beta.3 Release Notes

Release Date: May 5, 2026

🎉 What's New

🛡️ Audit-Driven Hardening & Type Safety

Lume.js v2.0.0-beta.3 is a pre-stable hardening release focused on closing all audited P0/P1 items that are safe to fix without a full benchmark regression suite. This release fixes TypeScript declaration bugs for consumers, patches a real memory leak in withPlugins, optimizes the effect cleanup hot path, and promotes the repeat addon from experimental to stable. No breaking changes from beta.2.


🏆 Highlights

  • TypeScript declarations fixed: Invalid Symbol('lume.reactive') replaced with proper unique symbol; withReadObserver now typed in public entry point
  • withPlugins memory leak patched: $dispose() method exposed to clean up $beforeFlush hooks — prevents unbounded growth when re-wrapping stores
  • Effect cleanup optimized: cleanups.splice(0) replaces [...cleanups] spread — zero iterator allocation per effect re-run
  • repeat() promoted to stable: @experimental JSDoc tag removed after 59 tests and zero open correctness issues in beta.2
  • Test Coverage: 319 tests | 100% stmts/branches/funcs/lines across all 16 source files
  • Bundle Size: 2.45KB gzipped core (81.5% of 3KB budget)

📦 Key Features

1. TypeScript Consumer-Facing Fixes

Invalid reactive brand symbol (index.d.ts)

The computed property name readonly [Symbol('lume.reactive')]?: true; was invalid TypeScript — Symbol() call expressions cannot appear in interface property positions. Replaced with a declared unique symbol.

// Before (beta.2): INVALID — TypeScript error for consumers
readonly [Symbol('lume.reactive')]?: true;

// After (beta.3):
declare const lumeReactiveSymbol: unique symbol;
readonly [lumeReactiveSymbol]?: true;

withReadObserver type declaration

Exported from src/core/state.js since beta.2 but missing from src/index.js and src/index.d.ts. Now discoverable with full (onRead, fn) signature.

// Now works:
import { withReadObserver } from 'lume-js';
// Type-safe signature is included in index.d.ts (marked @internal)

Stale JSDoc example corrected

The createDebugPlugin documentation still showed the removed state(obj, { plugins }) API from beta.1. Updated to the current withPlugins(state(obj), [...]) pattern.

2. withPlugins Memory Leak Fix

Root cause: withPlugins() registered runNotifyHooks via store.$beforeFlush() but never stored the returned unsubscribe. Since runNotifyHooks is a fresh closure per call, the $beforeFlush dedupe (indexOf === -1) couldn't prevent growth — each wrapper had a different function reference. In long-lived SPAs with route changes, this caused unbounded beforeFlushHooks growth.

Fix:

  • Capture flushUnsub = store.$beforeFlush(runNotifyHooks)
  • Expose store.$dispose() on the returned proxy that calls flushUnsub() and clears pendingNotifications
const store = withPlugins(state({ count: 0 }), [myPlugin]);
// ... later, on route change:
store.$dispose(); // removes beforeFlush hook, clears pending state

3. Effect Cleanup Performance

Before: const oldCleanups = [...cleanups]; — spreads the subscriptions array via the iterator protocol, allocating a new Array on every effect re-run.

After: const oldCleanups = cleanups.splice(0); — single native operation that returns removed items and empties the source array. Zero behavioral change; restore/dispose paths remain identical.

4. repeat() Stabilized

The keyed reconciler, focus/scroll preservation, and DOM lifecycle hooks have been stable since beta.2. The @experimental JSDoc tag has been removed — repeat() is now a fully supported addon.

5. New Addons (from beta.2 → reflected in docs)

  • createCleanupGroup() — collects cleanup/unsubscribe functions and disposes them all at once
  • hydrateState() — reads initial state from <script type="application/json"> for zero-config SSR hydration
  • Minified CDN bundlesdist/index.min.mjs, dist/addons.min.mjs, dist/handlers.min.mjs via Terser

📊 Stats & Improvements

Test Coverage

  • 319 tests | 100% stmts/branches/funcs/lines across all 16 source files
  • +16 tests added since beta.2 (2 new $dispose tests, addon coverage expansion)
  • Edge case coverage for: plugin disposal, multi-wrap isolation, type declaration validity

Bundle Size

  • Core: 2.45KB gzipped (81.5% of 3KB budget)
  • Core files:
    • core/bindDom.js — 1.04KB
    • core/effect.js — 0.48KB
    • core/state.js — 0.92KB

Performance

  • Effect cleanup: Eliminated per-run array iterator allocation — splice(0) vs [...cleanups] spread
  • withPlugins disposal: Proper cleanup path for dynamic plugin wrapping/unwrapping in SPAs

📚 Resources


⚙️ Installation

For Beta Testers

npm install lume-js@2.0.0-beta.3
<!-- CDN -->
<script type="module">
  import { state, bindDom } from 'https://unpkg.com/lume-js@2.0.0-beta.3';
  import { withPlugins, repeat } from 'https://unpkg.com/lume-js@2.0.0-beta.3/addons';
</script>

🚀 Migration Guide

From v2.0.0-beta.2 to v2.0.0-beta.3

No breaking changes. All APIs remain stable.

Optional improvements you can adopt:

  1. Dispose plugin-wrapped stores on teardown:

    const store = withPlugins(state({ count: 0 }), [myPlugin]);
    // On route change or component unmount:
    store.$dispose();
  2. TypeScript users: withReadObserver is now importable from lume-js with full type support (marked @internal — primarily for advanced use)

From v2.0.0-beta.1

See v2.0.0-beta.2 release notes for the state(obj, {plugins})withPlugins() migration.


⚠️ Beta Release Notice

This is a beta release intended for testing and feedback. The v2.0 API is stabilizing and approaching the stable release. No breaking changes are anticipated between beta.3 and v2.0.0 stable.


💬 Getting Help

Full Changelog: v2.0.0-beta.2...v2.0.0-beta.3

v2.0.0-beta.2

05 May 02:34

Choose a tag to compare

v2.0.0-beta.2 Pre-release
Pre-release

Lume.js v2.0.0-beta.2 Release Notes

Release Date: May 3, 2026

🎉 What's New

🏗️ Architecture Hardening & Core Decoupling

Lume.js v2.0.0-beta.2 is a major architectural overhaul focused on correctness, performance, and environmental safety. The core state.js module is now completely decoupled from effect.js via a scope-based read observer system, the plugin system has been moved out of core into a dedicated addon, and 11 critical bugs have been fixed across the core and addon layers.

👉 Explore the updated API docs: docs/api/core/state.md


🏆 Highlights

  • withReadObserver Scope-Based Read Tracking: state.js is now pure when no reader is active — no permanent reference to effect.js
  • Plugin System Moved to Addons: state(obj, { plugins })withPlugins(state(obj), [plugins]) from lume-js/addons
  • 11 Bug Fixes: Frozen objects, flush loop isolation, duplicate keys in repeat(), phantom options, brand symbol implementation, and more
  • Test Coverage: 303 tests | 100% stmts/branches/funcs/lines across all 15 source files
  • Bundle Size: 2.45KB gzipped core (81.5% of 3KB budget)
  • Performance: Eliminated double microtask flush in withPlugins, eliminated per-flush array allocations, replaced filter-based unsubscribe with indexOf+splice
  • Environment Hardening: Console-less environment safety via src/utils/log.js (service workers, embedded engines)

📦 Key Features

1. Scope-Based Read Tracking (withReadObserver)

The global registerEffectSystem has been replaced with a synchronous withReadObserver(onRead, fn) scope. Read tracking is only active during the body of an effect's execution. Multiple simultaneous observers (nested effects, devtools) are supported via a Set of active readers.

// Before (alpha.2 / beta.1):
// state.js held a permanent callback reference to effect.js

// After (beta.2):
// state.js has no knowledge of effect.js
// Read tracking is scoped to the synchronous fn body
withReadObserver(
  (store, key) => { /* register dependency */ },
  () => { /* effect body — reads are tracked here only */ }
);
  • state.js is pure when no reader is active
  • Eliminates third-party spoofing via globalThis.__LUME_CURRENT_EFFECT__ → replaced with module-scoped currentEffect

2. Plugin System Extracted to Addons

Plugins are no longer passed to state(). Use the new withPlugins() addon wrapper instead.

// Before (beta.1):
import { state } from 'lume-js';
const store = state(obj, { plugins: [myPlugin] });

// After (beta.2):
import { state } from 'lume-js';
import { withPlugins } from 'lume-js/addons';
const store = withPlugins(state(obj), [myPlugin]);
  • withPlugins() returns the same reactive proxy with plugin hooks attached
  • Zero runtime overhead if no plugins are used
  • isReactive() also moved to lume-js/addons (duck-typing via $subscribe check)

3. Critical Bug Fixes

Bug Fix
Frozen/sealed object rejection state() now throws early instead of silently crashing
Flush loop error isolation Scheduler recovers after catastrophic errors; flushScheduled always reset via try/finally
repeat() duplicate keys Duplicate keys are skipped instead of overwriting element references
withPlugins blocked writes onNotify no longer fires when onSet returns oldValue to block an update
state() phantom options Removed non-existent options parameter from TypeScript declaration
Runtime brand symbol Symbol.for('lume.reactive') is now actually stamped by state.js
bindDom error swallowing Removed blanket try/catch; unexpected errors propagate with full stack traces
repeat() subscribe cleanup Handles RxJS-style Subscription objects with .unsubscribe()
computed.dispose() Added disposed flag to cancel stale microtasks
$beforeFlush deduplication Same function reference registered multiple times no longer duplicates execution

📊 Stats & Improvements

Test Coverage

  • 303 tests | 100% stmts/branches/funcs/lines across all 15 source files
  • 54 new tests added since beta.1
  • Edge case coverage for: select-multiple binding, nested effect tracking, re-entry guards, handler overrides, plugin edge cases

Bundle Size

  • Core: 2.45KB gzipped (81.5% of 3KB budget)
  • Core files:
    • core/bindDom.js — 1.04KB
    • core/effect.js — 0.48KB
    • core/state.js — 0.92KB

Performance

  • Eliminated double microtask flush in withPlugins — collapsed onNotify + subscriber flushes into single microtask
  • Eliminated per-flush array allocations in state.flush — replaced spreads with stable-index while loops and pre-sized arrays
  • Replaced filter-based unsubscribe with indexOf+splice — O(n) without allocation

📚 Resources


⚙️ Installation

For Beta Testers

npm install lume-js@2.0.0-beta.2
<!-- CDN -->
<script type="module">
  import { state, bindDom } from 'https://unpkg.com/lume-js@2.0.0-beta.2';
  import { withPlugins } from 'https://unpkg.com/lume-js@2.0.0-beta.2/addons';
</script>

🚀 Migration Guide

From v2.0.0-beta.1 to v2.0.0-beta.2

⚠️ Breaking Changes

  1. Plugin System Moved to Addons

    // Before (beta.1):
    const store = state(obj, { plugins: [myPlugin] });
    
    // After (beta.2):
    import { withPlugins } from 'lume-js/addons';
    const store = withPlugins(state(obj), [myPlugin]);
  2. isReactive() Import Path Changed

    // Before (beta.1):
    import { isReactive } from 'lume-js';
    
    // After (beta.2):
    import { isReactive } from 'lume-js/addons';
  3. resolvePath() Removed from Public API

    • Internal utility inlined into bindDom.js
    • src/core/utils.js deleted

New Features You Can Adopt:

  1. withPlugins() Addon: Cleaner separation of concerns — core stays minimal
  2. Console-less Safety: Library now works in service workers and embedded engines
  3. Improved Error Propagation: bindDom no longer swallows unexpected errors

⚠️ Beta Release Notice

This is a beta release intended for testing and feedback. The v2.0 API is stabilizing but may still see minor refinements before the stable release.

💬 Getting Help

Full Changelog: v2.0.0-beta.1...v2.0.0-beta.2

v2.0.0-beta.1

05 May 02:33

Choose a tag to compare

v2.0.0-beta.1 Pre-release
Pre-release

Lume.js v2.0.0-beta.1 Release Notes

Release Date: February 28, 2026

🎉 What's New

🔧 Extensible Handler System for Reactive DOM Binding

Lume.js v2.0.0-beta.1 introduces a powerful, extensible handler system for bindDom that transforms how reactive attributes are applied to DOM elements. Instead of hardcoded attribute lists, you can now define and register custom reactive handlers with a simple plain-object contract.

👉 Explore the handler API: docs/api/core/handlers.md


🏆 Highlights

  • Extensible Handler System: Custom reactive data-* attribute handlers via plain-object contract
  • Built-in Handlers: data-hidden, data-disabled, data-checked, data-required, data-aria-* (backward compatible)
  • New lume-js/handlers Module: show, boolAttr(), ariaAttr(), classToggle(), stringAttr(), htmlAttrs() presets
  • Test Coverage: 249 tests (up from 193 in alpha.2) | 100% code coverage
  • Bundle Size: 2.39KB gzipped core (+60 bytes from handler interface, still within 3KB budget)
  • Performance: Optimized cleanup loops, hot-path fast-path checks, WeakMap-based binding storage
  • TypeScript: Full handler types, utility types, and enhanced definitions

📦 Key Features

1. Extensible Handler System

Handlers are plain objects with an attr string and an apply(el, val) function. No framework API required.

import { bindDom } from 'lume-js';

// Custom handler: toggle a CSS class based on state
const highlightHandler = {
  attr: 'data-highlight',
  apply(el, val) {
    el.classList.toggle('highlight', !!val);
  }
};

bindDom(document.body, store, {
  handlers: [highlightHandler]
});
  • User handlers override built-ins with the same attr (Map deduplication)
  • Arrays auto-flattened (supports classToggle() returning multiple handlers)
  • Compiled selectors built from handler list for O(n) DOM performance

2. New lume-js/handlers Module (0.33KB gzipped, tree-shakeable)

import { show, boolAttr, ariaAttr, classToggle, stringAttr, htmlAttrs } from 'lume-js/handlers';

// show — inverse of data-hidden
bindDom(container, store, { handlers: [show] });
// <div data-show="isVisible">...</div>

// boolAttr — any boolean attribute
bindDom(container, store, { handlers: [boolAttr('readonly')] });
// <input data-readonly="isReadOnly" />

// classToggle — CSS class toggling
bindDom(container, store, { handlers: [classToggle('active', 'error')] });
// <div data-class-active="isActive" data-class-error="hasError">...</div>

// htmlAttrs() preset — standard HTML attributes
import { htmlAttrs } from 'lume-js/handlers';
bindDom(container, store, { handlers: htmlAttrs });
  • showdata-show="key" shows element when truthy
  • boolAttr(name) — toggle any boolean attribute via toggleAttribute()
  • ariaAttr(name) — any ARIA attribute with auto aria- prefix handling
  • classToggle(...names) — CSS class toggling
  • stringAttr(name) — string attributes with null removal
  • formHandlers preset — [boolAttr('readonly')]
  • a11yHandlers preset — [ariaAttr('pressed'), ariaAttr('selected'), ariaAttr('disabled')]
  • htmlAttrs() preset — 20+ standard HTML attributes as reactive handlers

3. Internal Architecture Refactoring

  • bindDom refactored from hardcoded BOOLEAN_ATTRS/ARIA_ATTRS arrays to composable handler pattern
  • Zero breaking changes — existing code works without modification
  • WeakMap-based binding storage for memory efficiency

📊 Stats & Improvements

Test Coverage

  • 249 tests (up from 193 in alpha.2)
  • Added 48 new handler tests covering show, classToggle, boolAttr, ariaAttr, stringAttr, custom handlers, composition, overrides, presets, and edge cases
  • 100% code coverage maintained

Bundle Size

  • Core: 2.39KB gzipped
  • Budget: 3KB gzipped (temporarily raised from 2KB to accommodate new features)
  • Reason: Added handler interface and built-in handlers. Optimization planned for stable release.

Performance

  • Optimized cleanup loops in effect.js and computed.js
  • Added fast-path checks in state.js hot paths
  • WeakMap-based binding storage in bindDom.js

📚 Resources


⚙️ Installation

For Beta Testers

npm install lume-js@2.0.0-beta.1
<!-- CDN -->
<script type="module">
  import { state, bindDom } from 'https://unpkg.com/lume-js@2.0.0-beta.1';
  import { show, classToggle } from 'https://unpkg.com/lume-js@2.0.0-beta.1/handlers';
</script>

🚀 Migration Guide

From v2.0.0-alpha.2 to v2.0.0-beta.1

No breaking changes. All alpha.2 code continues to work as-is.

New Features You Can Adopt:

  1. Custom Handlers: Replace inline DOM manipulation with declarative handlers
  2. Handler Presets: Use htmlAttrs() for one-shot reactive HTML attribute binding
  3. Improved TypeScript: Enhanced types for handler interfaces and utility types

⚠️ Beta Release Notice

This is a beta release intended for testing and feedback. The API is largely stable but may still undergo refinement before v2.0.0 stable.

💬 Getting Help

Full Changelog: v2.0.0-alpha.2...v2.0.0-beta.1

v2.0.0-alpha.2

14 Jan 20:08

Choose a tag to compare

v2.0.0-alpha.2 Pre-release
Pre-release

Lume.js v2.0.0-alpha.2 Release Notes

Release Date: January 14, 2026

🎉 What's New

🔌 Explicit Effect Dependencies & Debugging Tools

Lume.js v2.0.0-alpha.2 brings significant enhancements to developer experience and control. We've introduced "Explicit Mode" for effects to give you fine-grained control over reactivity, a powerful new Debug Addon for inspecting your stores, and major performance optimizations for DOM binding.

👉 Explore the new Effect docs: docs/api/core/effect.md

🏆 Highlights

  • Explicit Effects: Dual-mode effect() support (Auto-tracking vs Explicit Tuples)
  • Debug Addon: Zero-config debugging tool with colored logs and stats
  • Performance: Event delegation in bindDom (N listeners → 1 listener)
  • Repeat API: Improved create/update separation for cleaner list rendering
  • Test Coverage: 193 passing tests (up from 162 in alpha.1)
  • Bundle Size: ~2.1KB gzipped (temporarily slightly over budget due to new features)

📦 Key Features

1. Explicit Effect Dependencies

You can now choose between "Magic" (auto-tracking) and "Explicit" (dependency array) modes.

import { state, effect } from 'lume-js';

const store = state({ count: 0, user: 'Alice' });

// Old way: Auto-tracking (still default!)
effect(() => {
  console.log(store.count);
});

// NEW: Explicit Mode
// Only re-runs when 'count' changes, even if we read other properties!
effect(() => {
  console.log(`User ${store.user} has count: ${store.count}`);
}, [[store, 'count']]); // <--- Only tracks 'count'

2. Debug Addon 🐞

A developer-friendly tool to inspect reactive state operations.

import { createDebugPlugin, debug } from 'lume-js/addons';

const store = state({ count: 0 }, {
  plugins: [createDebugPlugin({ label: 'my-store' })]
});

debug.enable(); // Turn on logging

store.count++;
// Console: [my-store] SET count: 0 → 1

Features:

  • Global Controls: debug.enable(), debug.filter('key')
  • Stats API: debug.logStats() shows a table of GET/SET/NOTIFY operations
  • Stack Traces: Find exactly where a state change originated

3. Event Delegation in bindDom

We've rewritten bindDom to use a single event listener on the root element instead of attaching listeners to every input.

  • O(1) lookup using Map
  • Huge memory savings for large forms
  • Completely internal change (no API breakage)

📚 Resources

📊 Stats & Improvements

Test Coverage

  • 193 tests (up from 162)
  • Added 6 new tests for Explicit Effects
  • Added 24 new tests for Debug Addon
  • 100% code coverage maintained

Bundle Size

  • Core: 2.14 KB gzipped (slightly over 2KB budget)
  • Reason: Added explicit dependency parsing and event delegation logic. Optimization planned for Beta release.

⚙️ Installation

For Early Adopters

npm install lume-js@next

⚠️ Alpha Release Notice

This is an alpha release intended for testing and feedback.
Not recommended for production until stable v2.0.0.

💬 Getting Help

Happy coding with Lume.js! 🌟

Full Changelog: v2.0.0-alpha.1...v2.0.0-alpha.2

v2.0.0-alpha.1

20 Dec 03:25

Choose a tag to compare

v2.0.0-alpha.1 Pre-release
Pre-release

Lume.js v2.0.0-alpha.1 Release Notes

Release Date: December 19, 2025

🎉 What's New

🔌 Plugin System (Phase 1)

Lume.js v2.0 introduces a powerful plugin system that allows you to extend and customize reactive behavior while maintaining the standards-only philosophy. This alpha release marks the first phase of our v2.0 roadmap.

👉 Explore the plugin system: docs/api/core/plugins.md

🏆 Highlights

  • Plugin System: 5 lifecycle hooks for complete control over state behavior
  • Backward Compatible: All v1.0 code works unchanged - plugins are opt-in
  • Type-Safe: Full TypeScript definitions for all plugin APIs
  • Documentation: Comprehensive plugin guide, design decisions, and 4 working examples
  • Bundle Size: 1.98KB / 2KB gzipped (slight increase, will optimize in Phase 2)
  • 100% Test Coverage: 148 passing tests (up from 114 in v1.0.0)

📦 Plugin System Features

5 Lifecycle Hooks

import { state } from 'lume-js';

const store = state({ count: 0 }, {
  plugins: [
    {
      // Called once when state is created
      onInit() {
        console.log('State initialized');
      },
      
      // Intercept property reads (effects track key, not value)
      onGet(key, value) {
        console.log('Get:', key, value);
        return value; // Can transform value
      },
      
      // Intercept property writes
      onSet(key, newValue, oldValue) {
        console.log('Set:', key, newValue);
        return newValue; // Return transformed value
      },
      
      // Called when a property subscription is added
      onSubscribe(key) {
        console.log('Subscribe:', key);
      },
      
      // Called before subscribers are notified
      onNotify(key, value) {
        console.log('Notify:', key, value);
      }
    }
  ]
});

store.count = 5; // Plugin hooks fire!

Hook Execution Order

  1. onGet → Transform value before effects track (effects track key access, not value)
  2. onSet → Validate/transform before update
  3. onSubscribe → Called when subscription is added
  4. onNotify → Called before each subscriber notification

Key Design Decisions

  • Effects track key access, not transformed values - Plugin onGet hooks run first, effects observe key access regardless of value transformations
  • Plugins are opt-in - Zero overhead if you don't use them
  • Array-based plugin order - Predictable execution (index 0 runs first)
  • Chain pattern - Each plugin receives output of previous plugin (onGet, onSet)
  • No magic - Standard JavaScript patterns only

📚 Example Plugins

1. Debug Plugin

Logs all state operations for development:

import { state } from 'lume-js';

const debugPlugin = {
  onSet(key, newValue, oldValue) {
    console.log(`[Debug] Set ${key}: ${oldValue}${newValue}`);
    return newValue;
  },
  onSubscribe(key) {
    console.log(`[Debug] Subscribed to "${key}"`);
  },
  onNotify(key, value) {
    console.log(`[Debug] Notifying subscribers of "${key}" = ${value}`);
  }
};

const store = state({ count: 0 }, {
  plugins: [debugPlugin]
});

store.count = 5;
// Logs: [Debug] Set count: 0 → 5
// Logs: [Debug] Subscribed to "count"
// Logs: [Debug] Notifying subscribers of "count" = 5

2. Validation Plugin

Enforces data constraints:

const validationPlugin = {
  onSet(key, newValue, oldValue) {
    if (key === 'email' && !newValue.includes('@')) {
      console.error('Invalid email:', newValue);
      return oldValue; // Keep old value on validation error
    }
    return newValue;
  }
};

const store = state({ email: 'user@example.com' }, {
  plugins: [validationPlugin]
});

store.email = 'invalid'; // Blocked! Email unchanged.

3. History Plugin

Time-travel debugging:

import { state } from 'lume-js';

const history = { undoStack: [], redoStack: [], isUndoRedo: false };

const historyPlugin = {
  onSet(key, newValue, oldValue) {
    if (!history.isUndoRedo && newValue !== oldValue) {
      history.undoStack.push({ key, value: oldValue });
      history.redoStack = []; // Clear redo stack on new change
    }
    return newValue;
  }
};

const store = state({ count: 0 }, { plugins: [historyPlugin] });

store.count = 1;
store.count = 2;
store.count = 3;

// Undo function
function undo() {
  if (history.undoStack.length === 0) return;
  const lastChange = history.undoStack.pop();
  history.redoStack.push({ key: lastChange.key, value: store[lastChange.key] });
  history.isUndoRedo = true;
  store[lastChange.key] = lastChange.value;
  history.isUndoRedo = false;
}

undo(); // count = 2
undo(); // count = 1

4. Transform Plugin

Normalize/sanitize data:

import { state } from 'lume-js';

const transformPlugin = {
  onSet(key, newValue) {
    if (key === 'username' && typeof newValue === 'string') {
      return newValue.toLowerCase().trim(); // Auto-normalize
    }
    return newValue;
  }
};

const store = state({ username: '' }, { plugins: [transformPlugin] });
store.username = '  ALICE  '; // Stored as "alice"
console.log(store.username); // "alice"

📚 Resources

📊 Stats & Improvements

Test Coverage

  • 148 tests (up from 114 in v1.0.0)
  • 34 new tests for the plugin system
  • 100% code coverage maintained
  • Plugin lifecycle tests, error handling, and integration tests

Bundle Size

  • Core: 1.98 KB / 2.00 KB gzipped (99.0% of budget)
  • Addons: 0.83 KB gzipped (unchanged from v1.0.0)
  • Note: Temporary 2KB budget - will decrease when effect/bindDom move to addons in Phase 2

Documentation

  • ✅ Comprehensive plugin guide (30+ examples)
  • ✅ Design decisions document updated
  • ✅ 4 working plugin examples (debug, validation, history, transform)
  • ✅ TypeScript definitions complete
  • ✅ README updated with v2.0+ markers

⚙️ Installation

For Early Adopters (v2.0.0-alpha.1)

npm install lume-js@next

For Production (v1.0.0 - Stable)

npm install lume-js

🚀 Migration Guide

From v1.0.0 to v2.0.0-alpha.1

No Breaking Changes! This is a backward-compatible feature release.

All v1.0 code works unchanged. The plugin system is opt-in:

// v1.0 code (still works)
const store = state({ count: 0 });

// v2.0 code (opt-in plugins)
const store = state({ count: 0 }, {
  plugins: [debugPlugin, validationPlugin]
});

⚠️ Alpha Release Notice

This is an alpha release intended for:

  • ✅ Early adopters who want to try the plugin system
  • ✅ Community feedback on the plugin API design
  • ✅ Testing in non-production environments

Not recommended for production until stable v2.0.0 is released.

🔧 API Reference

state(initialState, options)

function state<T extends Record<string, any>>(
  initialState: T,
  options?: {
    plugins?: Plugin[]
  }
): T & ReactiveState;

Plugin Interface

interface Plugin {
  name: string;
  onInit?(): void;
  onGet?(key: string | symbol, value: any): any;
  onSet?(key: string | symbol, newValue: any, oldValue: any): any;
  onSubscribe?(key: string | symbol): void;
  onNotify?(key: string | symbol, value: any): void;
}

Plugin Example

import { state } from 'lume-js';

// Simple debug plugin
const debugPlugin: Plugin = {
  name: 'debug',
  onSet(key, newValue, oldValue) {
    console.log(`Set ${String(key)}: ${oldValue}${newValue}`);
    return newValue;
  }
};

const store = state({ count: 0 }, {
  plugins: [debugPlugin]
});

Note: Plugins are simple objects - no helper functions needed. See docs/api/core/plugins.md for complete examples including debug, validation, history, and transform patterns.

🐛 Known Issues

None at this time. If you find a bug, please open an issue.

💬 Getting Help

  • Questions: Open a GitHub Discussion
  • Bugs: Open a GitHub Issue
  • Feature Requests: Open an issue with the enhancement label
  • Feedback: We'd love to hear your thoughts on the plugin system!

Happy coding with Lume.js! 🌟

Built for developers who want extensibility without the framework tax.

Full Changelog: v1.0.0...v2.0.0-alpha.1

v1.0.0

29 Nov 08:01

Choose a tag to compare

Lume.js v1.0.0 Release Notes

Release Date: November 29, 2025

🎉 What's New

🚀 First Stable Release

Lume.js is now production-ready, with a stable API, comprehensive documentation, and an official website:

👉 Explore the new site: sathvikc.github.io/lume-js/


🏆 Highlights

  • Stable API: All core and most addon APIs are now stable and ready for production.
  • Website Launch: Official docs, guides, and interactive examples now available at sathvikc.github.io/lume-js/.
  • Comprehensive Documentation: Full API, guides, and tutorials included.
  • 100% Test Coverage: 114 passing tests covering all features and edge cases.
  • Performance: <2KB gzipped, no virtual DOM, zero dependencies, standards-only reactivity.
  • Features:
    • Core: state, bindDom, effect, two-way binding, nested state, subscriptions
    • Addons: computed, watch, isReactive
    • repeat Addon: Experimental — efficient keyed list rendering, but API may evolve in future releases
    • TypeScript definitions for all APIs
    • Tree-shaking and bundler optimization
  • Examples: Todo app, Tic-Tac-Toe, repeat-test, and more

⚠️ Experimental Status

The repeat addon is marked experimental in v1.0.0:

  • API may evolve based on real-world usage feedback
  • Performance characteristics being validated in production scenarios
  • Edge cases may be discovered that require API adjustments

We encourage you to try it and provide feedback! The core functionality is solid and well-tested, but we want to gather user experience before locking the API.


📚 Resources


🐛 Known Issues

None at this time. If you find a bug, please open an issue.


💬 Getting Help

  • Questions: Open a GitHub Discussion
  • Bugs: Open a GitHub Issue
  • Feature Requests: Open an issue with the enhancement label

Happy coding with Lume.js! 🌟

Built for developers who want reactivity without the framework tax.


Full Changelog: v0.5.0...v1.0.0


v0.5.0

28 Nov 08:56

Choose a tag to compare

Lume.js v0.5.0 Release Notes

Release Date: November 28, 2025

🎉 What's New

List Rendering with repeat Addon (@experimental)

The star of this release is the new repeat addon - an efficient list rendering solution that brings the convenience of v-for/x-for while staying true to Lume's standards-only philosophy.

import { repeat } from 'lume-js/addons/repeat.js';

repeat('#list', store, 'todos', {
  key: todo => todo.id,
  render: (todo, el) => {
    if (!el.dataset.init) {
      el.innerHTML = `<input value="${todo.text}">`;
      el.dataset.init = 'true';
    }
  }
});

Key Features:

  • Element Reuse by Key - Same DOM nodes persist across updates (no recreation)
  • Focus Preservation - Maintains activeElement and text selection during updates
  • Scroll Preservation - Intelligent scroll position handling for add/remove/reorder
  • Automatic Subscription - Subscribes to array changes automatically
  • Minimal DOM Operations - Only updates what changed
  • Memory Efficient - Automatic cleanup on element removal

Important Note: The repeat addon requires immutable array updates for performance and simplicity:

// ❌ Won't trigger update
store.items.push(newItem);

// ✅ Triggers update
store.items = [...store.items, newItem];

This trade-off enables instant reference equality checks (oldArray === newArray) instead of expensive deep array proxying or monkey-patching.

Design Philosophy Documentation

Added comprehensive DESIGN_DECISIONS.md explaining:

  • Why standards-only approach matters
  • Why objects instead of primitives
  • Why immutable array updates in repeat
  • Why no custom event handling syntax
  • Why nested state must be explicitly wrapped
  • And many more architectural decisions

This document helps contributors understand the "why" behind Lume's design and serves as a guide for future feature proposals.


📊 Stats & Improvements

Test Coverage

  • 114 tests (up from 67 in v0.4.1)
  • 47 new tests for the repeat addon
  • 100% code coverage maintained
  • Added regression tests for array mutation behavior
  • Added integration tests for repeat + computed

Examples

Updated and expanded examples:

  • Todo App - Now uses repeat for list rendering
  • Tic-Tac-Toe - Demonstrates time travel and computed state
  • Repeat Test - Comprehensive automated test suite for preservation features
  • Comprehensive Example - Updated with latest patterns

Documentation

  • Clarified immutable array requirement warnings throughout
  • Added explicit @experimental marker for repeat addon
  • Improved README with better examples and API docs
  • Added Contributing section emphasizing design philosophy

🔧 API Reference

repeat(container, store, key, options)

Parameters:

  • container (String|HTMLElement) - CSS selector or element to render into
  • store (Object) - Reactive state object
  • key (String) - Property name of the array in store
  • options (Object):
    • key (Function) - Unique key extractor: item => item.id
    • render (Function) - Render function: (item, el) => { /* update el */ }
    • preserveFocus (Function|null) - Focus preservation strategy (default: built-in)
    • preserveScroll (Function|null) - Scroll preservation strategy (default: built-in)

Returns: Cleanup function

Example:

const cleanup = repeat('#todo-list', store, 'todos', {
  key: todo => todo.id,
  render: (todo, el) => {
    if (!el.dataset.init) {
      el.innerHTML = `
        <input type="checkbox" class="toggle">
        <label></label>
        <button class="destroy">×</button>
      `;
      el.dataset.init = 'true';
    }
    
    el.querySelector('.toggle').checked = todo.completed;
    el.querySelector('label').textContent = todo.text;
  }
});

// Later: cleanup when done
cleanup();

Disabling Preservation:

repeat('#list', store, 'items', {
  key: item => item.id,
  render: (item, el) => { el.textContent = item.name; },
  preserveFocus: null,    // Disable focus preservation
  preserveScroll: null    // Disable scroll preservation
});

🚀 Migration Guide

From v0.4.x to v0.5.0

No Breaking Changes! This is a minor version with new features only.

If you're manually rendering lists and want to try repeat:

Before (manual rendering):

store.$subscribe('items', (items) => {
  const container = document.querySelector('#list');
  container.innerHTML = items.map(item => `
    <li>${item.name}</li>
  `).join('');
});

After (using repeat):

import { repeat } from 'lume-js/addons/repeat.js';

repeat('#list', store, 'items', {
  key: item => item.id,
  render: (item, el) => {
    el.textContent = item.name;
  }
});

Important: Remember to update arrays immutably:

// Add item
store.items = [...store.items, newItem];

// Remove item
store.items = store.items.filter(item => item.id !== id);

// Update item
store.items = store.items.map(item => 
  item.id === id ? { ...item, completed: true } : item
);

// Reorder
store.items = [...store.items].sort((a, b) => a.order - b.order);

⚠️ Experimental Status

The repeat addon is marked @experimental because:

  • API may evolve based on real-world usage feedback
  • Performance characteristics being validated in production scenarios
  • Edge cases may be discovered that require API adjustments

We encourage you to try it and provide feedback! The core functionality is solid and well-tested, but we want to gather user experience before locking the API.


📦 Bundle Size

Core library remains under 2KB (~1.45 KB) gzipped


📚 Resources


🐛 Known Issues

None at this time. If you find a bug, please open an issue.


💬 Getting Help

  • Questions: Open a GitHub Discussion
  • Bugs: Open a GitHub Issue
  • Feature Requests: Open an issue with the enhancement label

Happy coding with Lume.js! 🌟

Built for developers who want reactivity without the framework tax.

Full Changelog: v0.4.1...v0.5.0

v0.4.1

21 Nov 03:39

Choose a tag to compare

Release Notes: v0.4.1

🐛 Bug Fixes & Stability Improvements

This patch release improves developer experience and distribution quality while maintaining full backward compatibility with v0.4.0.


🆕 New Features

1. isReactive() - Type Guard for Reactive Proxies

Check whether an object is a Lume.js reactive proxy created by state().

import { state, isReactive } from 'lume-js';

const original = { count: 1 };
const store = state(original);

isReactive(store);    // true
isReactive(original); // false
isReactive(null);     // false

Use Cases:

  • Conditional wrapping patterns
  • Type guards in TypeScript
  • Debugging and validation
  • Library integration

Implementation:

  • Zero mutation of the proxy
  • Uses internal symbol checked via Proxy get trap
  • Symbol is not enumerable or visible via Object.getOwnPropertySymbols
  • Fast: single identity check, no WeakSet lookups
  • Skips tracking for $-prefixed meta methods (e.g., $subscribe)

Example - Defensive Wrapping:

function ensureReactive(val) {
  return isReactive(val) ? val : state(val);
}

2. Auto-Ready DOM Binding (DOMContentLoaded Auto-Wait)

bindDom() now automatically waits for DOMContentLoaded if the document is still loading, eliminating timing issues.

// ✅ Safe to call from <head> or anywhere
import { state, bindDom } from 'lume-js';

const store = state({ count: 0 });
bindDom(document.body, store); // Auto-waits if needed!

Features:

  • ✅ No manual DOMContentLoaded event handling needed
  • ✅ Safe in <head>, inline <script>, or deferred scripts
  • ✅ Opt-out available with { immediate: true } option
  • ✅ Cleanup function works correctly in both modes

Advanced Usage:

// Force immediate binding (skip auto-wait)
const cleanup = bindDom(myElement, store, { immediate: true });

Benefits:

  • Zero friction for beginners
  • Eliminates "DOM not ready" errors
  • Works seamlessly with any script placement
  • Standards-based (uses native DOMContentLoaded event)

📦 Distribution Improvements

1. Package Exports Field (Tree-Shaking Optimized)

Added proper exports field for better bundler support and tree-shaking.

{
  "exports": {
    ".": {
      "import": "./src/index.js",
      "types": "./src/index.d.ts"
    },
    "./addons": {
      "import": "./src/addons/index.js",
      "types": "./src/addons/index.d.ts"
    }
  }
}

Benefits:

  • ✅ Explicit public API surface
  • ✅ Better tree-shaking in Webpack, Rollup, Vite
  • ✅ Prevents accidental deep imports
  • ✅ Future-proof for package modernization

2. Side-Effects Declaration

Marked package as side-effect-free for aggressive tree-shaking.

{
  "sideEffects": false
}

Impact:

  • Unused exports are fully removed by bundlers
  • Smaller production bundles
  • Better optimization opportunities

3. Separate Addon TypeScript Definitions

Created src/addons/index.d.ts for addon-specific types, ensuring proper IntelliSense when importing from lume-js/addons.

// Full type safety with addon imports
import { computed, watch } from 'lume-js/addons';

Structure:

  • Core types: src/index.d.ts (state, bindDom, effect, isReactive)
  • Addon types: src/addons/index.d.ts (computed, watch)
  • Shared types imported via relative path

📚 Documentation Updates

1. README Enhancements

  • ✅ Added isReactive() API documentation with examples
  • ✅ Documented auto-ready bindDom() behavior
  • ✅ Added "When to use immediate: true" guidance
  • ✅ Clarified script placement scenarios
  • ✅ Updated TypeScript usage examples

2. DESIGN_DECISIONS.md Updates

  • ✅ Documented isReactive() implementation rationale
  • ✅ Explained $-prefixed method exclusion from effect tracking
  • ✅ Added auto-ready design decision
  • ✅ Document history updated

3. TypeScript Improvements

  • ✅ Complete type definitions for all v0.4.0+ features
  • ✅ Proper Computed<T> interface with error handling
  • ✅ Generic type safety for watch()
  • ✅ JSDoc examples for better IntelliSense

🔧 Internal Improvements

1. Effect Tracking Refinement

  • Skip tracking for $subscribe and other $-prefixed methods
  • Prevents meta-method access from creating spurious dependencies
  • Cleaner separation between API methods and reactive properties

2. Reactive Marker Implementation

  • Uses symbol-based identity check via Proxy get trap
  • No WeakSet or external storage needed
  • Invisible to enumeration and reflection
  • Zero performance overhead

3. Cleanup Pattern Consistency

  • All subscription APIs return unsubscribe functions
  • Cleanup works correctly before and after DOMContentLoaded
  • Proper event listener removal in auto-ready mode

📊 Bundle Size

Core library remains under 2KB (~1.45 KB) gzipped


⚙️ Testing

  • ✅ 67 tests passing (100% coverage maintained)
  • ✅ New tests for isReactive()
  • ✅ New tests for auto-ready bindDom() behavior
  • ✅ Tests for immediate: true option
  • ✅ Tests for cleanup before DOMContentLoaded
  • ✅ Effect tracking exclusion tests

🔄 Migration Guide

From v0.4.0 to v0.4.1

No breaking changes - fully backward compatible!

New Features You Can Adopt:

1. Use isReactive() for Type Guards:

import { state, isReactive } from 'lume-js';

function processData(data) {
  if (!isReactive(data)) {
    data = state(data);
  }
  // Now guaranteed to be reactive
}

2. Simplify Script Placement:

<!-- Before: Manual DOMContentLoaded handling -->
<script type="module">
  document.addEventListener('DOMContentLoaded', () => {
    bindDom(document.body, store);
  });
</script>

<!-- After: Just call bindDom() -->
<script type="module">
  bindDom(document.body, store); // Auto-waits!
</script>

3. TypeScript Addon Imports:

// Now fully typed
import { computed, watch } from 'lume-js/addons';

🐛 Known Issues

None. This is a stability release.


📝 Full Changelog

Added:

  • isReactive(obj) function for reactive type checking
  • Auto-ready bindDom() with DOMContentLoaded auto-wait
  • { immediate: true } option for bindDom()
  • exports field in package.json
  • sideEffects: false declaration
  • src/addons/index.d.ts for addon types
  • Comprehensive TypeScript definitions
  • CHANGELOG.md consolidating release history

Fixed:

  • Effect tracking now skips $-prefixed meta methods
  • Cleanup function works correctly in auto-ready mode
  • TypeScript addon import resolution

Improved:

  • Documentation clarity for new features
  • Type safety for all APIs
  • Tree-shaking optimization
  • Developer experience for script placement

📅 Release Date

November 20, 2025


🔗 Links

Full Changelog: v0.4.0...v0.4.1

v0.4.0

17 Nov 03:55

Choose a tag to compare

🎉 Major Release - Automatic Dependency Tracking

This is a significant upgrade that brings automatic reactivity to Lume.js while maintaining the standards-only philosophy.


⚡ Breaking Changes

computed() Now Uses Automatic Dependency Tracking

Before (v0.3.0):

const store = state({ count: 0 });
const doubled = computed(() => store.count * 2);

// Had to manually recompute
store.$subscribe('count', () => doubled.recompute());

After (v0.4.0):

const store = state({ count: 0 });
const doubled = computed(() => store.count * 2);

// Auto-updates when store.count changes - no manual recompute!
store.count = 5;
console.log(doubled.value); // 10 (after microtask)

Migration: Remove all manual .recompute() calls and $subscribe calls used for recomputation. The computed() function now automatically tracks dependencies and updates.


🚀 New Features

1. effect() - Core Automatic Dependency Tracking

New core primitive for creating reactive effects that automatically track which state properties they access.

import { state, effect } from 'lume-js';

const store = state({ count: 0, name: 'Alice' });

const cleanup = effect(() => {
  // Automatically tracks 'count' (name not accessed)
  document.title = `Count: ${store.count}`;
});

store.count = 5;    // Effect re-runs automatically
store.name = 'Bob'; // Effect does NOT re-run

cleanup(); // Stop the effect

Features:

  • ✅ Automatic dependency collection
  • ✅ Dynamic dependencies (re-tracks on every run)
  • ✅ Returns cleanup function
  • ✅ Prevents infinite recursion
  • ✅ Integrates with per-state batching

2. Per-State Microtask Batching

State updates are now automatically batched using microtasks for optimal performance.

const store = state({ count: 0 });

effect(() => {
  console.log('Count:', store.count);
});

// Multiple updates in same tick
store.count = 1;
store.count = 2;
store.count = 3;

// Effect only runs ONCE in next microtask with final value (3)

Benefits:

  • ✅ Reduces unnecessary re-renders
  • ✅ Better performance for rapid updates
  • ✅ Per-state batching (no global scheduler)
  • ✅ Effects deduplicated per flush cycle

3. Enhanced computed() with Auto-Tracking

The computed() addon now uses effect() internally for automatic dependency tracking.

import { computed } from 'lume-js/addons';

const cart = state({
  items: [
    state({ price: 10, quantity: 2 }),
    state({ price: 15, quantity: 1 })
  ]
});

const total = computed(() => {
  return cart.items.reduce((sum, item) => 
    sum + (item.price * item.quantity), 0
  );
});

console.log(total.value); // 35

cart.items[0].quantity = 3;
// Auto-updates to 45 in next microtask

New API:

  • .value - Get computed value (cached)
  • .subscribe(callback) - Watch for changes (returns unsubscribe)
  • .dispose() - Cleanup and stop tracking
  • .recompute() - REMOVED (automatic now)

🐛 Bug Fixes

Better Error Handling

  • effect() errors logged with [Lume.js effect] prefix
  • computed() errors logged with [Lume.js computed] prefix
  • ✅ Errors set computed value to undefined

📚 Documentation Updates

Enhanced README

  • ✅ Added effect() documentation and examples
  • ✅ Updated computed() docs for automatic tracking
  • ✅ Added comparison of reactive patterns (effect vs subscribe)
  • ✅ New examples showcasing auto-tracking
  • ✅ Updated cleanup patterns
  • ✅ Better TypeScript definitions

New Sections

  • Testing guide with Vitest
  • Bundle size information
  • Effect vs Subscribe decision guide
  • Advanced reactivity patterns

🔧 Technical Changes

State Management (src/core/state.js)

  • ✅ Added globalThis.__LUME_CURRENT_EFFECT__ for dependency tracking
  • ✅ Proxy getter now registers effect dependencies
  • ✅ Per-state pendingNotifications Map for batching
  • ✅ Per-state pendingEffects Set for deduplication
  • scheduleFlush() uses queueMicrotask()

Effect System (src/core/effect.js) - NEW

  • ✅ Automatic dependency collection via proxy getters
  • ✅ Re-tracks dependencies on every execution
  • ✅ Cleanup old subscriptions before re-running
  • ✅ Prevents infinite recursion with isRunning flag
  • ✅ Returns cleanup function

Computed (src/addons/computed.js)

  • ✅ Now uses effect() internally
  • ✅ Removed manual .recompute() method
  • ✅ Auto-tracks dependencies
  • ✅ Caches values and notifies subscribers on change
  • .dispose() method for cleanup

TypeScript Definitions (src/index.d.ts)

  • ✅ Added effect() type definition
  • ✅ Updated computed() return type (removed recompute)
  • ✅ Better generic type support
  • ✅ Unsubscribe type alias

📊 Stats

  • 57 tests with 100% coverage
  • Bundle size: Still under 2KB gzipped ✅

🎯 Migration Guide

If you used computed():

Remove manual recomputation:

const doubled = computed(() => store.count * 2);

- store.$subscribe('count', () => doubled.recompute());
+ // No longer needed - automatic!

Remove .recompute() calls:

- doubled.recompute();
+ // Auto-updates when dependencies change

If you used $subscribe() for derived state:

Consider switching to effect():

- const unsub1 = store.$subscribe('firstName', updateTitle);
- const unsub2 = store.$subscribe('lastName', updateTitle);
+ const cleanup = effect(() => {
+   document.title = `${store.firstName} ${store.lastName}`;
+ });

If you manually batched updates:

Remove batching code:

- let timeout;
- store.$subscribe('value', (val) => {
-   clearTimeout(timeout);
-   timeout = setTimeout(() => updateUI(val), 0);
- });
+ effect(() => {
+   updateUI(store.value); // Auto-batched!
+ });

⚠️ Notes

Effect Execution Timing

Effects run in the next microtask after state changes. If you need synchronous updates, use $subscribe() instead.

// Async (batched) - preferred for most cases
effect(() => {
  console.log(store.count); // Runs in next microtask
});

// Sync (immediate) - use when timing matters
store.$subscribe('count', (val) => {
  console.log(val); // Runs immediately
});

Per-State Batching

If an effect depends on multiple state objects, it may run once per state that changes (by design). This keeps the architecture simple and predictable.

const store1 = state({ a: 1 });
const store2 = state({ b: 2 });

effect(() => {
  console.log(store1.a + store2.b);
});

// Both change in same tick
store1.a = 10;
store2.b = 20;

// Effect runs TWICE (once per state flush)
// This is intentional - keeps effects simple

Full Changelog: v0.3.0...v0.4.0