Skip to content

tracked() architecture: per-component signal scoping for React#32

Merged
scottmessinger merged 82 commits intomainfrom
feature/vite-compiler-plugin
Mar 18, 2026
Merged

tracked() architecture: per-component signal scoping for React#32
scottmessinger merged 82 commits intomainfrom
feature/vite-compiler-plugin

Conversation

@scottmessinger
Copy link
Copy Markdown
Member

@scottmessinger scottmessinger commented Mar 14, 2026

Summary

  • Replace useTracked with tracked() — a component wrapper that gives each component its own signal subscription scope
  • Fix array mutation bugs (splice/pop/shift were completely broken)
  • Add $VERSION as a signal for efficient array structural tracking
  • Remove all failed experiments (DirectFor, $$(), createView, vite-plugin)
  • Set up oxfmt + oxlint with pedantic + style categories

The tracked() architecture

tracked() wraps a component definition. It sets currentSub once before the component function runs and restores it after. All reactive proxy reads during the component's render are tracked to that component's own effect.

const Row = tracked(({ item, isSelected }) => {
  return (
    <tr className={isSelected ? 'danger' : ''}>
      <td>{item.id}</td>
      <td>{item.label}</td>
    </tr>
  )
})

Key property: Component(props) is synchronous — children render later in separate React calls. So currentSub doesn't leak into children. Each tracked() component independently subscribes to exactly the signals it reads.

Result: label change on row 5 re-renders only Row 5. App doesn't re-render. No 1000-item memo check cascade.

Benchmark results (apples-to-apples, same Row template)

Operation useTracked (old) tracked() (new) delta
create 1000 57.9ms 40.3ms 30% faster
replace all 61.7ms 52.0ms 16% faster
partial update 18.0ms 2.4ms 7.5x faster
select row 5.6ms 3.8ms 32% faster
swap rows 40.6ms 8.7ms 4.7x faster
remove row 7.6ms 6.0ms 21% faster
create 10000 759ms 615ms 19% faster
append 1000 51.0ms 55.5ms ~same
clear 10.7ms 10.5ms same

Faster or equal on every operation. Zero regressions.

Core changes

  • deleteProperty on arrays: splice/pop/shift were throwing "Direct deletion not allowed." Fixed to allow silent deletes with ownKeys bump.
  • $VERSION as signal: bumpVersion writes to a signal. Alien-signals deduplicates dirty-marking so 1000 writes during splice cost ~0.2ms (vs 999 ownKeys bumps at ~30ms).
  • Array version auto-subscribe: When the reactive proxy returns an array with an active subscriber, automatically subscribe to the version signal. Splice, push, swap all detected.
  • trackSelf on array function access: .map(), .forEach() etc. subscribe to ownKeys when there's a subscriber.

Removed (failed experiments)

  • packages/vite-plugin/ — $$() compiler transform (double work with React)
  • packages/js-krauset-direct/ — DirectFor benchmark (catastrophic on structural mutations)
  • packages/react-example/ — unused example
  • DirectFor — full DOM rebuild, 788x slower on remove
  • $$() / useDirectBindings — bypasses React but does double work, or requires store-coupled components
  • createView / useView — marginal improvement (0.07% of reads benefit from getter vs proxy)
  • useScopedTracked — correct concept but proxy wrapper slower than tracked()
  • use-sync-external-store — vestigial from old useTracked

Tooling

  • oxfmt for formatting (replaces prettier)
  • oxlint with pedantic + style categories on src/ (replaces eslint)
  • CI updated with lint and format:check steps
  • Removed eslint, prettier configs

Test plan

  • 188 tests pass across all packages
  • 0 typecheck errors
  • 0 lint errors (pedantic + style on src/)
  • All files formatted (oxfmt)
  • tracked() correctness: per-component scoping, label isolation, structural operations
  • tracked() safe on non-reactive components (acts as memo())
  • Nested tracked() components have independent subscriptions
  • Krauset benchmark compliance tests (9 operations)

🤖 Generated with Claude Code

scottmessinger and others added 8 commits March 13, 2026 20:53
Prototypes proving feasibility of compiled store reads:
- Branded<T> type works with complex real-world interfaces
- TypeScript compiler API detects $BRAND on PropertyAccessExpressions
- Compiled reads 1.2-4.5x faster than solid-js/store
- Direct signal reads with readSignal() helper
- State library comparison benchmarks

See issue #31 for full plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
createStore now returns [Branded<T>, SetStoreFunction] where Branded<T>
recursively adds a phantom $BRAND marker at every object nesting level.
This enables the Vite compiler plugin to identify store objects at
compile time via the TypeScript type checker.

- Export $BRAND symbol and Branded<T> type from @supergrain/core
- Brand is optional ([$BRAND]?: true) so plain objects remain assignable
- Array items are branded, arrays themselves are not
- Fix pre-existing typecheck errors in benchmarks
- Rename prototype type test to .typetest.ts (not a vitest suite)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
readSignal(target, prop) returns the signal for a property, creating it
lazily on $NODE. The Vite plugin compiles store.title into
readSignal(store, 'title')(). Accepts proxies or raw objects (calls
unwrap internally). Shares signals with the proxy get trap.

Also exports setProperty from @supergrain/core for direct writes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vite plugin that compiles branded store property reads into direct
signal access: store.title → readSignal(store, 'title')().

Uses TypeScript compiler API to detect $BRAND on resolved types.
Handles nested access, skips writes/method calls, auto-inserts
readSignal import. MagicString for source-mapped edits.

6 tests covering branded reads, nested access, writes, plain objects,
method calls, and import merging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simplified tracking hook for use with the Vite compiler plugin.
Sets currentSub so compiled readSignal() calls track to the
component's effect — no tracking proxy needed.

useTrackedStore remains for backward compatibility (proxy-based
tracking without the compiler plugin).

3 tests: identity return, reactive re-renders, fine-grained tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- hasBrand now checks for 'supergrain:brand' specifically instead of
  matching any __@ prefixed symbol property
- useTracked saves/restores the previous subscriber via useLayoutEffect,
  preventing leaked subscribers across component boundaries
- Document dev mode limitation in vite plugin (stale TS program)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ESLint rule that flags React components using readSignal() without
calling useTracked(). Pattern-based detection on compiled output —
if a function body has readSignal calls but no useTracked call, it
reports an error. Same enforcement pattern as rules-of-hooks.

7 test cases: 4 valid, 3 invalid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
readSignal inside .map() callbacks no longer flagged when the parent
component has useTracked. exitFunction checks ancestor scopes and
propagates hasReadSignal upward.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 13:23
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-supergrain Error Error Mar 16, 2026 9:13pm
v0-supergrain-5971 Ready Ready Preview, Comment, Open in v0 Mar 16, 2026 9:13pm

Request Review

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a “compiled reads” pipeline for @supergrain/core stores: createStore returns a branded type to enable a Vite TypeScript transform that rewrites store.foo reads into direct signal access via readSignal(...)(). It also adds a simplified React hook (useTracked) meant to work with compiled reads, plus an ESLint rule to enforce useTracked usage when readSignal is used.

Changes:

  • Add Branded<T> + $BRAND to createStore types and introduce readSignal(target, prop) in core.
  • Add @supergrain/vite-plugin that rewrites branded property reads into readSignal(...)() and injects imports.
  • Add useTracked in @supergrain/react and an @supergrain/eslint-plugin rule (require-use-tracked) with tests.

Reviewed changes

Copilot reviewed 53 out of 55 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
vitest.config.ts Adds a dedicated node-environment project for eslint-plugin tests.
tsconfig.json Adds TS project references for new workspace packages (vite-plugin, eslint-plugin).
packages/vite-plugin/vite.config.ts Library build config for the new Vite plugin package.
packages/vite-plugin/tsconfig.json TS config for vite-plugin (composite/declarations/outDir).
packages/vite-plugin/tests/transform.test.ts Unit tests for branded-read transform behavior.
packages/vite-plugin/src/plugin.ts TS compiler API transform + import injection + Vite plugin wrapper.
packages/vite-plugin/src/index.ts Exports supergrain() plugin entrypoint.
packages/vite-plugin/package.json New package metadata/deps/scripts for @supergrain/vite-plugin.
packages/react/tests/use-tracked.test.tsx React tests validating useTracked + readSignal re-render behavior.
packages/react/src/use-store.ts Adds useTracked hook intended for compiled reads.
packages/react/src/index.ts Exports useTracked from @supergrain/react.
packages/js-krauset/src/main.tsx Updates imports to include $BRAND/Branded (currently unused).
packages/eslint-plugin/vitest.config.ts Vitest config for eslint-plugin tests.
packages/eslint-plugin/tsconfig.json TS config for eslint-plugin (composite/outDir).
packages/eslint-plugin/tests/require-use-tracked.test.ts Tests for require-use-tracked rule.
packages/eslint-plugin/src/rules/require-use-tracked.ts ESLint rule implementation enforcing useTracked with readSignal.
packages/eslint-plugin/src/index.ts ESLint plugin entry exporting rules.
packages/eslint-plugin/package.json New eslint-plugin package metadata (currently points main to TS sources).
packages/core/tests/read-signal.test.ts New tests validating readSignal reactivity + proxy interop.
packages/core/tests/branded-type.test.ts Type-level tests for Branded<T> + runtime sanity checks.
packages/core/src/store.ts Adds $BRAND, Branded<T>, readSignal, and updates createStore return type.
packages/core/src/index.ts Exports readSignal, setProperty, $BRAND, and Branded.
packages/core/prototype/vite-plugin.ts Prototype plugin (regex-based) for earlier experimentation.
packages/core/prototype/vite-plugin-bench/vite.config.ts Prototype bench Vite config.
packages/core/prototype/vite-plugin-bench/src/bench.ts Prototype bench harness for rewritten vs proxy reads.
packages/core/prototype/vite-plugin-bench/check-transform.ts Script to print prototype transform output.
packages/core/prototype/realistic.bench.ts Prototype “realistic” benchmark scenarios.
packages/core/prototype/preact-store.ts Prototype preact-signals store implementation for benchmarking.
packages/core/prototype/preact-store-correctness.test.ts Correctness tests for preact-store prototype.
packages/core/prototype/plugin-feasibility/input.ts Feasibility input for TS compiler detection of branding.
packages/core/prototype/plugin-feasibility/detect-branded.ts Feasibility script for brand detection via TS checker.
packages/core/prototype/model.ts Prototype model implementation and benchmarks scaffold.
packages/core/prototype/model.test.ts Prototype model correctness tests.
packages/core/prototype/model.bench.ts Prototype model benchmarks.
packages/core/prototype/direct-signal-reads.bench.ts Benchmarks for “direct signal read” approach.
packages/core/prototype/direct-signal-correctness.test.ts Correctness tests for “direct signal read” approach.
packages/core/prototype/computed-vs-signal.bench.ts Benchmarks comparing computed vs signal call overheads.
packages/core/prototype/compiled-vs-stores.bench.ts Benchmarks comparing compiled approach vs other stores.
packages/core/prototype/compiled-reads.bench.ts Benchmarks focused on compiled reads.
packages/core/prototype/compiled-reads-and-writes.bench.ts Benchmarks for compiled reads plus compiled writes.
packages/core/prototype/compiled-correctness.test.ts Correctness tests for benchmark scenarios.
packages/core/prototype/compiled-alien-vs-preact.bench.ts Benchmarks comparing alien vs preact primitives under same architecture.
packages/core/prototype/check-versions.mjs Script to print installed versions for benchmark context.
packages/core/prototype/call-overhead.bench.ts Benchmarks for per-call overhead in effects.
packages/core/prototype/branded-type.typetest.ts Large type-level validation suite for Branded<T>.
packages/core/package.json Updates core package metadata/deps/scripts (includes version change).
packages/core/benchmarks/state-libraries.bench.ts Adds cross-library benchmarks (zustand/jotai/mobx/valtio/preact).
packages/core/benchmarks/STATE_LIBRARIES.md Documents benchmark results/interpretation.
packages/core/benchmarks/ROW_OPERATIONS.md Documents row operation benchmark results.
packages/core/benchmarks/CORE_COMPARISON.md Documents core vs solid-js/store benchmark results.
packages/core/benchmarks/ADDITIONAL.md Documents additional benchmark results and analysis.
packages/core/PLAN-model-api.md Design/plan doc for the compiled reads approach and plugin.
notes/failed-approaches/inline-primitive-checks-optimization.md Postmortem doc for a reverted micro-optimization attempt.
.claude/settings.local.json Updates local Claude tool allowlist (dev-only config).
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/eslint-plugin/package.json Outdated
Comment thread packages/core/package.json
Comment thread packages/js-krauset/src/main.tsx Outdated
Comment thread packages/vite-plugin/tests/transform.test.ts Outdated
Comment thread packages/vite-plugin/src/plugin.ts Outdated
Comment thread packages/react/src/use-store.ts Outdated
Unified to one hook name: useTracked. The proxy-based implementation
works with or without the Vite compiler plugin. No separate hooks
for compiled vs proxy paths.

Also adds packages/js-krauset-compiled/ for A/B benchmarking the
compiled reads path against the proxy path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
scottmessinger and others added 14 commits March 14, 2026 14:50
- Add vitest workspace config to packages/core with two projects: proxy
  (no plugin) and compiled (with supergrain vite plugin). Every core test
  now runs in both modes automatically.
- Fix vite plugin import injection: add readSignal to the existing import
  source instead of hardcoding @supergrain/core (fixes dual-module bug).
- Change readSignal to return wrap(node()) so bracket access and dynamic
  property reads work correctly through compiled boundaries.
- Add typed overload to readSignal so the plugin can resolve Branded<T>
  through chained readSignal calls.
- Change plugin to build nested readSignal expressions for full property
  chains (store.a.b.c → readSignal(readSignal(readSignal(store,'a'),'b'),'c')).
- Add compiled-comparison.bench.ts benchmarking proxy vs compiled reads.
- Delete packages/core/prototype/ (replaced by real plugin + benchmarks).
- Document per-level readSignal approach in notes/failed-approaches/.
- Upgrade vitest 3.2.4 → 4.1.0 across all packages.
- Fix vitest 4 breaking changes: browser.provider factory, context imports,
  matchers setup, node_modules exclusion, tsconfig types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core changes:
- Add readLeaf() — skips wrap() for primitive reads (no proxy creation)
- Add initSignals() — eagerly creates signals for all properties at
  createStore time and when new objects are set via setProperty. Required
  for inlined $NODE access to work without fallback branches.
- Make $NODE configurable to avoid proxy invariant violations
- Add typed overload to readSignal preserving Branded<T> through chains
- Export readLeaf from @supergrain/core

Benchmarking:
- Add real-overhead.bench.ts isolating per-operation costs:
  cached $NODE is 14x faster than proxy for 3 leaf reads
- Update compiled-comparison.bench.ts (reactive scenarios only)
- Hand-write compiled krauset benchmark (js-krauset-compiled) showing
  what the compiler should produce: useCompiled + direct $NODE reads
- Add compiled-vs-proxy.test.tsx in react package: correctness tests
  for all krauset operations (create, update, select, swap, remove, clear)
  in both proxy and compiled modes

Key finding: direct signal call via cached $NODE is 10-14x faster than
proxy, but the plugin can't yet generate this form reliably. The
hand-written compiled krauset benchmark demonstrates the target output.

Known limitation: compiled mode doesn't detect array mutations ($pull,
splice) because the data signal reference doesn't change — needs
$OWN_KEYS tracking which the compiled path doesn't have yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Captures key findings from earlier conversations:
- Alien-signals vs preact-signals benchmarks (preact 2-4x faster reads)
- Why solid-js is faster (compiler eliminates proxy reads, 60x proxy overhead)
- Computed caching architecture (zero overhead, beat solid in every benchmark)
- Transcript locations for future agent exploration
- Open questions that were never resolved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…analysis

Proxy optimization:
- Add Solid-style fast path to proxy get trap: check for already-tracked
  signal BEFORE symbol checks. On repeat reads (the common case), this
  is 3 operations instead of ~13. Improves proxy reads by ~12%.

Class getter discovery:
- V8 inlines class prototype getters to near-bare-signal speed (4,506 ops/s)
  but CANNOT inline: function calls (541), Object.defineProperty getters (1,142),
  or proxy traps (472). This is the same reason preact signals (.value getter)
  are 2-4x faster than alien-signals (function call).
- Class getter reads are 9.5x faster than proxy for reactive leaf reads
  and 6.3x faster for component render (6 props).
- Hand-written StoreView class demonstrates the target compiler output.

Research documentation:
- notes/research/path-to-10x.md: Full synthesis of findings with action plan
- notes/research/prior-conversation-findings.md: Transcript locations and
  prior research summary
- benchmarks/exhaustive-read-patterns.bench.ts: Every possible read pattern
  ranked by ops/s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All three modes (proxy, compiled, class-getter) pass all 6 krauset
operations: create 1000 rows, update every 10th, select, swap, remove,
clear. 66 tests total.

Class getter approach uses a hand-written AppStateView class with
prototype getters that call signals directly. V8 inlines these to
~4,500 ops/s (10x faster than proxy at ~470 ops/s in micro-benchmarks).

Refactored test to share Row component and effect setup across modes.
useClassView hook caches view instances per raw object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Real browser benchmark results (chromium via playwright):
- Create 1000 rows: class-getter 1.14x faster
- Update every 10th: class-getter 1.40x faster
- Select row: class-getter 1.07x faster
- Swap rows: class-getter 1.56x faster

The 10x micro-benchmark advantage translates to 1.1-1.6x end-to-end
because React's reconciliation dominates total render time. Store reads
are a small fraction of the work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
End-to-end browser benchmark (chromium, all operations):
- Create 1000 rows: class-getter 1.17x faster (57ms vs 67ms)
- Replace 1000 rows: class-getter 1.09x faster
- Partial update (100/1000): class-getter 1.21x faster
- Select row: class-getter 1.26x faster (54ms vs 68ms)
- Swap rows: class-getter 1.57x faster (60ms vs 94ms)
- Clear rows: class-getter 1.09x faster

10x micro-benchmark advantage translates to 10-25% end-to-end because
React reconciliation dominates. But consistent improvement across all
operations — worth pursuing if combined with other optimizations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three approaches benchmarked end-to-end in browser:
- proxy: useTracked + For (current)
- class-getter: AppStateView with V8-inlined getters
- fine-grained: each row subscribes to 'selected' independently

Results:
- Create: fine-grained 1.24x faster (fewer parent re-renders)
- Select: class-getter 1.10x faster (fine-grained causes 1000 effect checks)
- Swap: class-getter 1.94x faster
- Partial update: class-getter 1.23x faster

Fine-grained has a tradeoff: faster initial render but slower on
selection changes because every row's effect fires. Class-getter
is the most consistent winner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createView(store) returns a lightweight view object with prototype
getters that read signals directly, bypassing the proxy. View objects
are cached per raw object (WeakMap) and prototypes are cached per
property-key set (Map).

Benchmark confirmed: dynamic prototype getters match static class
getters in V8 (~4,100 ops/s, 8x faster than proxy). The critical
requirement is getters on a PROTOTYPE, not on instances.

End-to-end krauset browser benchmark results (chromium):
- Create 1000 rows: createView 1.37x faster than proxy
- Swap rows: createView 1.46x faster
- Partial update: createView 1.48x faster
- Select row: tied (within variance)

All 142 core tests pass (proxy + compiled modes).
All 66 react tests pass.
All packages green.

Getter pattern benchmark at packages/core/benchmarks/getter-patterns.bench.ts
confirms all dynamic prototype approaches (Function+prototype,
setPrototypeOf, new Function class) perform identically to static
class getters. Only per-instance defineProperty is slower (4x).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createModelStore(schema, data) returns [proxy, update, view] where
the view prototype is built from ArkType schema props at definition
time. Nested schemas generate nested view prototypes recursively.

Core micro-benchmarks:
- Model store leaf reads: 4,836 ops/s (10.2x faster than proxy)
- Model store 6-field render: 1,184 ops/s (2.4x faster than proxy)
- Reactive updates/batching: equivalent to proxy

9 new tests for model-store: schema creation, reactive reads, writes,
nested views, sub-tree replacement, fine-grained isolation, prototype
sharing. All pass.

Cleaned up krauset browser benchmark to remove model-store entries
(arktype not available in browser context). createView entries remain
as the browser-testable prototype-getter approach.

160 core tests, 66 react tests, all packages green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DirectRow subscribes signals to DOM refs via useEffect, bypassing
React's reconciliation for subsequent updates. Initial render still
goes through React.

Results: direct-dom shows modest improvement on select (1.15x) and
swap (1.74x), but is slower on partial update — the per-row effect
subscription setup cost (2 effects × 1000 rows) dominates in the
current benchmark structure where create+operate are measured together.

The direct-dom approach's real advantage is on subsequent updates
after initial render, which this benchmark doesn't isolate well.
The setup cost of 2000 effect subscriptions per iteration masks the
DOM update savings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Solid-style approach: React renders outer container, rows created via
cloneNode(true) + direct signal subscriptions to DOM nodes. No React
components, no VDOM, no memo for rows.

Results (chromium, end-to-end):
- Create 1000 rows: 2.3x faster (31ms vs 71ms)
- Select row: 2.3x faster (32ms vs 73ms)
- Swap rows: 3.6x faster (27ms vs 95ms)
- Partial update: 1.8x faster (39ms vs 70ms)

This is the performance ceiling for a React + signals architecture.
It matches solid-js's approach: template cloning + direct DOM bindings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
$$() is an identity function exported from @supergrain/core. It marks
reactive expressions for direct DOM binding. Without the compiler, it
returns the value unchanged (graceful degradation). The compiler will
transform $$() calls in JSX to ref + effect subscriptions.

useDirectBindings hook (from @supergrain/react) takes an array of
{ ref, getter, attr? } bindings and wires alien-signals effects
directly to DOM nodes, bypassing React re-renders:
- Text position: updates ref.current.textContent
- Attribute position: updates ref.current[attr]

6 new tests: text updates, className updates, multiple bindings,
zero React re-renders, cleanup on unmount, computed expressions.

160 core tests, 72 react tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plugin now detects $$() calls in JSX and transforms them into
ref + useDirectBindings for direct DOM updates bypassing React.

Transformation:
  {$$(item.label)}     → ref on parent element + useDirectBindings text binding
  className={$$(...)}  → ref on element + useDirectBindings attr binding

For each component containing $$() calls, the plugin:
1. Generates useRef declarations (__$$0, __$$1, ...)
2. Generates useDirectBindings([...]) with all bindings
3. Adds ref={__$$N} to target JSX elements
4. Strips $$() wrapper, leaving just the inner expression
5. Injects useRef (react) and useDirectBindings (@supergrain/react) imports

Arrow function expressions in $$(() => expr) are preserved as getters.
Plain expressions in $$(expr) are wrapped in () => expr.

10 plugin tests pass (6 existing + 4 new).
160 core tests, 72 react tests unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
scottmessinger and others added 25 commits March 16, 2026 21:30
DirectFor now uses alien-signals effect to watch array structure
(length + element references). Catches in-place mutations (splice,
push, swap) that don't change the array reference.

Key fixes:
- useLayoutEffect depends on [each] for reference changes (store.data = x)
- Inner effect subscribes to every array index for structural changes
- Row effects created outside reactive context (no nested overhead)
- Cleanup tears down both outer + row effects

Added 5 new tests matching krauset operations:
- create 1000 rows, swap rows, remove row, append rows, clear
All 95 react tests pass. Zero type errors.

Also fixed js-krauset-direct to use containerRef + forceUpdate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert .claude/settings.local.json to main to keep local-only
permissions out of the PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move all planning docs to appropriate folders (architecture,
  performance, failed-approaches, research) based on status
- Delete path-to-10x.md (fully superseded by compiled-reads-investigation.md)
- Remove .vitest-attachments from tracked files
- Add .claude/settings.local.json to .gitignore
- Clean up duplicate .vitest-attachments gitignore entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
V2 contains the root cause analysis, before/after numbers, and
complete solution. V1 adds no unique context and would confuse
future agents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add TL;DR/status block at top of every doc
- Standardize structure: goal, approach, outcome, learnings
- Failed approaches: prefix titles with FAILED, surface failure
  reason immediately
- Remove redundant prose (~7500 lines net reduction)
- Convert verbose paragraphs to tables where data is involved
- Fix cross-references for moved files (planning/ → current paths)
- Update README.md to reflect current folder structure
- Comparisons: cut 50-70% verbose source citations, keep data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- v2-initial-design: cut from 670 to ~95 lines, mark as historical
- v4/nested-components-solution: trim redundancy, point to v5
- v5-final: add missing cleanup useEffect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Architecture: add status blocks, trim ember-analysis and
react-adapter-architecture (~60% reduction), mark superseded content.

Benchmarks: add TL;DR to all 14 files, convert prose to tables,
note superseded/duplicated docs, cut verbose recommendations.

Failed approaches: add FAILED: prefix to all 6 titles, surface
failure reasons front-and-center, collapse verbose step-by-step
narratives into tables (~70-85% reduction per file).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…undant docs

Structure changes:
- Eliminate research/ folder — files distributed to where they belong
- compiled-reads-investigation → performance/ (capstone perf doc)
- reactively-takeaways → merged into comparisons/reactively.md
- ember-analysis → comparisons/ember.md (external system analysis)
- signal-pooling-benchmark-code → benchmarks/signal-pooling.md

File renames for clarity:
- core-benchmarks-readme → running-benchmarks
- consolidated-findings → findings-summary
- js-benchmark-plan → krausest-setup
- performance-plan-v2 → core-store-optimization
- v5-final → useTracked (name the thing, not the version)
- solid-architecture → solid, storable → supergrain
- techniques → reactive-techniques

Merges:
- structurae-evaluation + structurae-final-assessment → single file
- reactively + reactively-takeaways → single file

Deletions (10 redundant files):
- benchmarks: analysis, benchmarks, proxy-overhead-{summary,benchmark},
  results, performance-analysis (all superseded)
- research: main-plan (unstarted), prior-conversation-findings (redundant)
- react-adapter: nested-components-solution (redundant with useTracked)

All cross-references updated across remaining files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…es doc

Restored (had unique content, not redundant):
- benchmarks/performance-analysis.md — benchmark bug correction methodology
- benchmarks/results.md — proxy vs direct signal data (referenced by findings-summary)
- benchmarks/proxy-overhead-benchmark.md — benchmark source code

Added:
- failed-approaches/react-tracking-approaches.md — documents all 7 failed
  React tracking approaches with failure reasons (extracted from v2-v4 history)

Updated:
- README.md — comprehensive index with descriptions, categorized failed approaches
- benchmarks/README.md — restored references to all files in the folder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The file was renamed but the internal reference still pointed to the
old name. Updated to point to README.md and fixed the H1 title.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core fixes:
- Fix deleteProperty on arrays: splice/pop/shift were throwing
  "Direct deletion not allowed". Now allows silent deletes on arrays
  with ownKeys bump for structural subscribers.
- Make $VERSION a signal: bumpVersion now writes to a signal in the
  node map instead of incrementing a plain number. This enables
  components to subscribe to "any mutation on this array" without
  subscribing to every individual index signal. Alien-signals
  deduplicates dirty-marking so 1000 writes during splice cost ~0.2ms.
- Auto-subscribe to array version: when the reactive proxy returns an
  array value with an active subscriber, automatically subscribe to
  the array's version signal. This ensures parent components detect
  in-place mutations (splice, push, swap) without explicit $TRACK reads.
- trackSelf on array function access: calling .map/.forEach/etc on a
  reactive proxy array subscribes to ownKeys when getCurrentSub is set.

These changes enable the tracked() architecture where each component
independently subscribes to exactly the signals it reads, achieving
7.5x faster partial updates and 4.7x faster swaps vs the current
useTracked approach.

Benchmark packages:
- js-krauset: add krauset compliance tests (9 ops) + perf tests
- js-krauset-direct: add exports, vitest config, compliance + perf tests
- react: add splice, swap, append, clear, partial update tests

Vite plugin:
- Fix CJS output extension (.cjs) for type: module packages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Detailed walkthrough of 5 experimental approaches:
- DirectFor (full DOM rebuild) — failed, catastrophic on structural mutations
- createView/useView (getter-based reads) — failed, marginal improvement
- $$() direct bindings — failed, double work with React
- useScopedTracked (per-component proxy) — partial, overhead from proxy wrapping
- tracked() component wrapper — success, 7.5x partial update, 4.7x swap

Includes source code, benchmark numbers, bug analysis, and key lessons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds: splice bug discovery, version signal vs ownKeys tradeoff,
stale dependency analysis, why For is still needed, why console.log
fixed the bug, per-field component alternative, explicit store.selected
read pattern, safe-on-non-reactive-components analysis, and
ships-vs-doesn't-ship summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds: alien-signals background, side-by-side label change trace (old
vs new architecture), "wrap the render not the reads" key insight
bridging Experiments 4→5, .map() vs For experiment (5b), why For's
version-based memo is redundant with tracked(), concrete stale
dependency example, clearer store.selected boundary explanation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes alien-signals background (covered in react-tracking-approaches.md),
redundant createStableProxy code (in useTracked.md), verbose experiment
code. Adds references to existing docs, connects tracked() to previously
rejected "global subscriber" approach (#1 in react-tracking-approaches.md),
adds "For as render boundary" insight. ~30% shorter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tracked() wraps the component definition instead of being called inside
it. Sets currentSub once per render (O(1)) instead of per property
access (O(n)). Each component independently subscribes to exactly the
signals it reads — parent reads don't leak into children.

Result: label change on row 5 re-renders only Row 5 (not App + 1000
memo checks). 7.5x faster partial updates, 4.7x faster swaps.

Removed:
- useTracked hook
- createStableProxy, globalProxyCache, proxyEffectMap
- All useTracked imports and usage across 25 files

Added:
- tracked() in packages/react/src/tracked.ts
- 11 tracked() tests in packages/react/tests/tracked.test.tsx

Converted: all tests, benchmarks, examples, and doc-tests to tracked().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README: replace all useTracked examples with tracked() pattern
- For component: remove version prop cloning (redundant with tracked()),
  simplify to key-only cloneElement for keyed reconciliation
- tracked.ts: remove unnecessary `as unknown as FC<P>` cast, let
  TypeScript infer MemoExoticComponent return type from memo()
- useTracked.md: mark as superseded by tracked()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- js-krauset main.tsx: remove (tracked as any) casts (types resolve
  after react package rebuild), remove stale version prop JSDoc
- write.ts: remove stale useView reference in deleteProperty comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- for-component-magic.test.tsx: remove versionSymbol lookup (For no
  longer passes version props)
- render-analysis.test.tsx: remove $VERSION usage, version props, and
  two tests that were entirely about the old version prop mechanism

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updated 13 docs: README index, architecture docs (react-adapter,
vite-compiler-plugin, app-store-plan), comparison docs (reactively,
solid, zustand, valtio, supergrain, rxjs, mobx, jotai). Left
failed-approaches/ and historical react-adapter/ versions unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tooling:
- Add oxfmt for formatting (216 files formatted)
- Remove eslint config (.eslintrc.json) and prettier (.prettierrc,
  .prettierignore) — replaced by oxlint + oxfmt
- Add root scripts: lint, format, format:check
- Update lint scripts to -D correctness -D suspicious on src/ only
- Fix React types in benchmark packages (19.0.0 → ^19.1.13)

Lint fixes:
- Remove unused imports (memo, FC, useReducer, useCallback, etc.)
- Remove unused variables and parameters (prefix with _)
- Remove dead versionSymbol code from for-component-magic test
- Remove dead version prop tests from render-analysis test
- Fix direct-for.tsx unused useReducer import

Notes docs:
- Update 13 docs to reference tracked() instead of useTracked

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ugin

Deleted packages:
- packages/vite-plugin/ — only purpose was $$() compiler transform
- packages/js-krauset-direct/ — used DirectFor (failed experiment)
- packages/react-example/ — unused example package

Deleted files:
- react/src/direct-for.tsx — full DOM rebuild, catastrophic on mutations
- react/src/use-direct-bindings.ts — $$() and useDirectBindings
- react/tests/direct-for.test.tsx, direct-binding.test.tsx, pipeline.test.tsx
- core/tests/read/create-view.test.ts

Removed from source:
- createView, getSignalGetter, defineSignalGetter, viewCache,
  signalGetterCache from core/src/read.ts
- createView exports from core/src/store.ts and core/src/index.ts
- DirectFor, $$, useDirectBindings exports from react/src/index.ts

Updated:
- README.md: removed $$(), createView, DirectFor sections
- CI: added lint and format:check steps
- Lint scripts: enabled pedantic + style categories on src/
- .changeset/config.json: removed react-example from ignore list
- tsconfig.json: removed vite-plugin reference
- vitest.config.ts: removed react-example test inclusion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add .oxlintrc.json disabling inappropriate rules (no-magic-numbers,
id-length, no-ternary, sort-keys, sort-imports, func-style, no-null,
func-names, init-declarations, max-lines, max-lines-per-function,
new-cap).

Code fixes across all packages:
- Add curly braces to all single-line if/for/while bodies
- Replace Object.prototype.hasOwnProperty.call with Object.hasOwn
- Replace for-in with for-of Object.keys()
- Use T[] instead of Array<T>
- Use TypeError for type-checking errors
- Reduce function params (options objects)
- Reduce nesting depth (extract helpers)
- Use arrow body style, destructuring, template literals
- Merge duplicate imports
- Use consistent type definitions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete packages/app-store/ (orphaned directory, no source code)
- Remove use-sync-external-store from root and react package
  (vestigial from old useTracked, not imported anywhere)
- Remove @types/use-sync-external-store from react devDependencies
- Fix doc-tests vitest.node.config.ts: app-store/src → store/src

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@scottmessinger scottmessinger changed the title Vite compiler plugin: compile store reads into direct signal access tracked() architecture: per-component signal scoping for React Mar 18, 2026
scottmessinger and others added 3 commits March 18, 2026 09:54
- tracked.ts: null out ref.current on cleanup so StrictMode
  remount creates a fresh effect (fixes dead effectNode bug)
- core.ts: remove unused .$ property from Signal interface and
  getNode (set but never read anywhere)
- read.ts: fix misleading "frozen objects" comment — the early
  return handles frozen objects, the catch handles sealed/
  non-configurable objects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- tracked.ts: null ref.current on cleanup so StrictMode remount
  creates a fresh effect (#1 from review)
- store/types.ts + document-promise.ts: expose error getter on
  DocumentPromise interface and implementation (#9)
- store/store.ts: memoize findDoc by (modelType, id) to avoid
  creating duplicate computeds. Replace spread copy of document
  map with direct path set (#5)
- core/core.ts: remove unused .$ property from Signal (#12)
- core/read.ts: fix misleading frozen objects comment (#11)
- core/operators.ts: clarify pullFromArray comment — operates on
  raw array, manual signal management is necessary (#7)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enable 7 previously-disabled lint rules (require-await, init-declarations,
  func-names, new-cap, unicorn/text-encoding-identifier-case,
  unicorn/prefer-global-this, unicorn/no-useless-undefined)
- Fix violations: name anonymous functions in typed.ts, inline disables for
  legitimate exceptions (React component calls, signal setters)
- Remove lint from doc-tests package (only core/react/store are linted)
- Enable import sorting in oxfmt with grouped ordering
- Fix @ts-nocheck placement in benchmark files after import reordering
- Remove .claude/settings.local.json from git tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@scottmessinger scottmessinger merged commit c403124 into main Mar 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants