diff --git a/.changeset/fix-history-and-react-selectors.md b/.changeset/fix-history-and-react-selectors.md
new file mode 100644
index 0000000..9dba4e5
--- /dev/null
+++ b/.changeset/fix-history-and-react-selectors.md
@@ -0,0 +1,5 @@
+---
+'@codebelt/classy-store': patch
+---
+
+Restore mutable nested state after undo/redo while preserving reactive collection methods and keeping recorded snapshots unchanged. Refresh React selections when selectors or equality functions change, even when the store has not mutated. Update the library's development dependencies and add regression coverage for both fixes.
diff --git a/.changeset/next-app-router-boundaries.md b/.changeset/next-app-router-boundaries.md
new file mode 100644
index 0000000..2e4b83d
--- /dev/null
+++ b/.changeset/next-app-router-boundaries.md
@@ -0,0 +1,9 @@
+---
+'@codebelt/classy-store': minor
+---
+
+Add a server-safe `@codebelt/classy-store/react/server` entry for `prepareStoreForHydration`, and mark the published React hooks entry with `use client`. The existing preparation export remains available from `/react`; both use the same capture cache within a module graph.
+
+Fix `Snapshot` function classification so methods with typed parameters retain their callable signatures under strict TypeScript, including reactive collection queries, overloads, and generic functions.
+
+Add packed-package production Next.js App Router tests with React Compiler off/on for package boundaries, request isolation, controlled inputs, streaming hydration, navigation, refresh, and cleanup, plus CI coverage and integration documentation.
diff --git a/.changeset/persist-operation-ordering.md b/.changeset/persist-operation-ordering.md
new file mode 100644
index 0000000..41ffc09
--- /dev/null
+++ b/.changeset/persist-operation-ordering.md
@@ -0,0 +1,7 @@
+---
+'@codebelt/classy-store': patch
+---
+
+Keep asynchronous persistence writes and removals in request order. Reads wait for prior writes and ignore results superseded by a newer read, save, clear, or local edit. Clearing storage cancels pending auto-saves, and cleanup skips queued operations that have not started.
+
+Ignore outdated browser storage events so two-tab synchronization cannot overwrite a newer local save with an older event. Add deterministic async regressions and real two-tab React tests in Chromium, Firefox, and WebKit.
diff --git a/.changeset/prepare-react-hydration.md b/.changeset/prepare-react-hydration.md
new file mode 100644
index 0000000..e208c78
--- /dev/null
+++ b/.changeset/prepare-react-hydration.md
@@ -0,0 +1,7 @@
+---
+'@codebelt/classy-store': minor
+---
+
+Add `prepareStoreForHydration(store)` to the React entry point. Call it on server and client stores initialized with matching data, before rendering or persistence, to retain the initial snapshot for server rendering and delayed Suspense hydration. Hooks then catch up to the live store without replacing server DOM, even if persistence changes state before their first read.
+
+Preparation returns the original mutable proxy, preserves the first capture on repeated calls, and keeps current client subscriptions and core snapshots unchanged. Equal selector results retain their identity when hydration switches to live state. Add delayed and streamed hydration regressions, React 18/19 package coverage, and usage documentation.
diff --git a/.changeset/react-compiler-tracking.md b/.changeset/react-compiler-tracking.md
new file mode 100644
index 0000000..859be82
--- /dev/null
+++ b/.changeset/react-compiler-tracking.md
@@ -0,0 +1,7 @@
+---
+'@codebelt/classy-store': patch
+---
+
+Fix stale auto-tracked React views when React Compiler or manual memoization reuses a derived value or child without repeating its property reads. Preserve committed dependencies for each reused immutable snapshot object, including structurally shared children, while isolating reads from suspended or abandoned renders.
+
+Add compiled-consumer package tests for React 18 and 19, browser input and memoized-child regressions, and independent concurrency, hydration, and property-operation coverage. Selector APIs and synchronous React notification behavior are unchanged. Auto-tracking can conservatively retain earlier reads until a snapshot object's identity changes.
diff --git a/.changeset/react-snapshot-target-cache.md b/.changeset/react-snapshot-target-cache.md
new file mode 100644
index 0000000..19d65ff
--- /dev/null
+++ b/.changeset/react-snapshot-target-cache.md
@@ -0,0 +1,5 @@
+---
+'@codebelt/classy-store': patch
+---
+
+Reuse guarded backing copies of frozen snapshots across React auto-tracked consumers. Large forms and lists avoid repeatedly scanning and copying the same snapshot array while each hook retains its own tracked reads. Reflective mutations of tracking targets are rejected, and mutable computed values remain uncached.
diff --git a/.changeset/release-retired-store-data.md b/.changeset/release-retired-store-data.md
new file mode 100644
index 0000000..15a2f8f
--- /dev/null
+++ b/.changeset/release-retired-store-data.md
@@ -0,0 +1,13 @@
+---
+'@codebelt/classy-store': patch
+---
+
+**Runtime requirement:** auto-tracked React hooks now require native `WeakRef` support (ES2021). Selector hooks do not require `WeakRef`. See the [0.7.0 upgrade guide](https://codebelt.github.io/classy-store/docs/UPGRADING_0_7).
+
+Release removed child proxies from their former parents, including children removed by native array-length truncation. Failed writes and deletes preserve the existing child connection; partial array truncations invalidate only the removed entries.
+
+Discard obsolete untracked snapshot clones when a nested object starts using versioned tracking. This releases initial collection data and prevents fresh stores over the same raw object from receiving the stale cached clone.
+
+Use weak render-read keys and a weak commit queue to prevent auto-tracked React consumers from retaining obsolete collection snapshots through shared child proxies. Isolate stable subscription and selector callbacks from render-local closure contexts so they do not retain initial results for the lifetime of a mount.
+
+Add deterministic lifecycle and Compiler regressions plus production Chromium heap-snapshot checks for mounts, resets, rolling replacements, truncation, and intentionally retained detached children with Compiler off and on.
diff --git a/.changeset/scoped-store-cleanup.md b/.changeset/scoped-store-cleanup.md
new file mode 100644
index 0000000..982da41
--- /dev/null
+++ b/.changeset/scoped-store-cleanup.md
@@ -0,0 +1,7 @@
+---
+'@codebelt/classy-store': patch
+---
+
+Ignore pending persistence read results and read-error callbacks after unsubscribe. Retired handles no longer overwrite a current React scope or clear storage when an expired read completes after effect cleanup. Pending hydration promises still settle when storage completes.
+
+Add a typed context example with independent and nested store instances, keyed resets, effect-owned persistence, and SSR hydration preparation. Test the actual example against packed React 18/19 consumers, including Strict Mode, storage configuration changes, asynchronous cleanup, and delayed Suspense hydration.
diff --git a/.changeset/synchronous-react-subscriptions.md b/.changeset/synchronous-react-subscriptions.md
new file mode 100644
index 0000000..3e16221
--- /dev/null
+++ b/.changeset/synchronous-react-subscriptions.md
@@ -0,0 +1,11 @@
+---
+"@codebelt/classy-store": minor
+---
+
+**Upgrade required for React consumers using timing options.** React subscriptions now always notify synchronously, fixing controlled-input caret jumps without configuration. Remove the `sync` option from `useClassyStore` and hooks returned by `createStoreHook`, and remove imports of `UseClassyStoreOptions`. Remove the entire options argument in auto-tracked mode, including an empty `{}`. Selector options still accept `isEqual`. Ordinary `subscribe()` callbacks remain microtask-batched and retain their existing `sync` option.
+
+See the [0.7.0 upgrade guide](https://codebelt.github.io/classy-store/docs/UPGRADING_0_7) for before/after examples, the new native `WeakRef` requirement for auto-tracking, and SSR, Next.js, and persistence guidance.
+
+React selectors may run after each mutation; several writes are not an atomic transaction. React can batch the resulting renders within an event handler. Auto-tracked reads are isolated per render so conditional and abandoned renders do not retain stale dependencies, including for class and null-prototype snapshots. Unchanged tracked references remain stable across parent renders.
+
+Persistence hydration restores mutable nested defaults and custom merged values while preserving prototypes and user accessors. Add real-browser input regressions, lifecycle and SSR coverage, and packed-package checks for React 18 and 19.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 934a767..a541f2e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,9 +8,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '24'
- uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: '1.4.3'
- run: bun install --frozen-lockfile
- run: bun run lint
- run: bun run test
- - run: bun run typecheck
+ # Packed-consumer typechecks resolve the package's generated declarations.
- run: bun run build
+ - run: bun run typecheck
+ - run: bunx playwright install --with-deps chromium firefox webkit
+ working-directory: packages/classy-store
+ - run: bun run test:browser
+ - name: React scale commit budgets (production, Compiler off/on)
+ run: bun packages/classy-store/tests/performance/runPerformance.ts
+ env:
+ BENCHMARK_SAMPLES: '1'
+ BENCHMARK_ITERATIONS: '3'
+ BENCHMARK_LABEL: ci-smoke
+ - name: React memory retention (production, Compiler off/on)
+ run: bun packages/classy-store/tests/memory/runMemory.ts
+ env:
+ MEMORY_CYCLES: '6'
+ MEMORY_SIZE: '24'
+ MEMORY_LABEL: ci-smoke
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index f795302..eb70626 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -18,7 +18,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '24'
- uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: '1.4.3'
- run: bun install --frozen-lockfile
- run: bun run build
- run: bun run demos:build
diff --git a/.github/workflows/react-compatibility.yml b/.github/workflows/react-compatibility.yml
new file mode 100644
index 0000000..c689298
--- /dev/null
+++ b/.github/workflows/react-compatibility.yml
@@ -0,0 +1,25 @@
+name: React compatibility
+
+on:
+ pull_request:
+ branches: [main]
+
+jobs:
+ react-compatibility:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ react: ['18.0.0', '18.3.1', '19.3.1']
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: '1.4.3'
+ - run: bun install --frozen-lockfile
+ - run: bun run build
+ - run: node packages/classy-store/tests/compatibility/runCompatibility.mjs "${{ matrix.react }}"
+ - run: node packages/classy-store/tests/compiler/runCompiler.mjs "${{ matrix.react }}"
diff --git a/.github/workflows/react-next.yml b/.github/workflows/react-next.yml
new file mode 100644
index 0000000..6552a40
--- /dev/null
+++ b/.github/workflows/react-next.yml
@@ -0,0 +1,38 @@
+name: React Next.js integration
+
+on:
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ next:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ strategy:
+ fail-fast: false
+ matrix:
+ compiler: ['off', 'on']
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: '1.4.3'
+ - run: bun install --frozen-lockfile
+ - run: bunx playwright install --with-deps chromium
+ working-directory: packages/classy-store
+ - name: Packed production Next.js consumer (Compiler ${{ matrix.compiler }})
+ run: bun run test:react:next
+ env:
+ NEXT_TEST_MODES: ${{ matrix.compiler }}
+ - name: Save Next.js build and browser diagnostics
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: react-next-compiler-${{ matrix.compiler }}
+ path: test-results/next
+ if-no-files-found: ignore
+ retention-days: 7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4fb1787..2175774 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,12 +16,14 @@ jobs:
steps:
- uses: actions/checkout@v6
with: { fetch-depth: 0 }
- - uses: oven-sh/setup-bun@v2
- - run: bun install --frozen-lockfile
- - run: bun run build
- uses: actions/setup-node@v6
with:
node-version: '24'
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: '1.4.3'
+ - run: bun install --frozen-lockfile
+ - run: bun run build
- name: Create Release PR or Detect Publishable Versions
id: changesets
uses: changesets/action@v1
diff --git a/.gitignore b/.gitignore
index 453e907..c7ff628 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,7 @@ website/build
website/.docusaurus
website/.cache-loader
website/static/demos
+
+# Browser test artifacts
+playwright-report
+test-results
diff --git a/README.md b/README.md
index dcfe064..b926fea 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,12 @@
Visit the **[Documentation Website](https://codebelt.github.io/classy-store/)** for tutorials, API reference, and examples.
+## Upgrading to 0.7.0
+
+React subscriptions now notify synchronously. Remove the React hooks' `sync` option and any `UseClassyStoreOptions` imports. Auto-tracked hooks now require native `WeakRef` support (ES2021). Selector equality options remain available, and ordinary core subscriptions still batch by default.
+
+See the [0.7.0 upgrade guide and release notes](https://codebelt.github.io/classy-store/docs/UPGRADING_0_7) for before/after examples, SSR and Next.js setup, and persistence changes.
+
## π Features
- **Class-Based**: Define state and logic using standard ES6 classes.
@@ -55,10 +61,10 @@ function AddButton() {
}
```
-For React controlled inputs, opt the field subscription into synchronous timing so React can preserve caret position and IME composition:
+React subscriptions notify synchronously, so controlled inputs preserve caret position without extra options:
```tsx
-const name = useClassyStore(formStore, (state) => state.name, {sync: true});
+const name = useClassyStore(formStore, (state) => state.name);
return (
=17.0.0",
@@ -178,7 +182,7 @@
"@angular/compiler-cli": ["@angular/compiler-cli@19.2.21", "", { "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", "tslib": "^2.3.0", "yargs": "^17.2.1" }, "peerDependencies": { "@angular/compiler": "19.2.21", "typescript": ">=5.5 <5.9" }, "bin": { "ngc": "bundles/src/bin/ngc.js", "ngcc": "bundles/ngcc/index.js", "ng-xi18n": "bundles/src/bin/ng_xi18n.js" } }, "sha512-eAs+krq6McyuoySyC0tbYuY+rtovdtmyNUAgxGANPYJMdkTYHcHdF0HVbbaxUpFDvRC6rV9NDpZUazgykLZoYA=="],
- "@angular/core": ["@angular/core@21.1.4", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/compiler": "21.1.4", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, "optionalPeers": ["@angular/compiler", "zone.js"] }, "sha512-QBDO5SaVYTVQ0fIN9Qd7US9cUCgs2vM9x6K18PTUKmygIkHVHTFdzwm4MO5gpCOFzJseGbS+dNzqn+v0PaKf9g=="],
+ "@angular/core": ["@angular/core@22.2.3", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/compiler": "22.2.3", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, "optionalPeers": ["@angular/compiler", "zone.js"] }, "sha512-ponarFrAZsPutT+htRt3PEmkt2/IB7DyrBy2f48sxsIx3ZA6xw09i2HOEBo+VBphlN6B/ynK9q+o04HPhdJ+dQ=="],
"@angular/forms": ["@angular/forms@19.2.21", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.21", "@angular/core": "19.2.21", "@angular/platform-browser": "19.2.21", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-2r5V8JyInrmpLhBDKCmF2ELWUXxEVxyIm5wDqD7RxWnuGtEr08QlJmiuyqM58xWFmcaSx7s2c62OdCqAAUSQGg=="],
@@ -188,17 +192,17 @@
"@angular/router": ["@angular/router@19.2.21", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "19.2.21", "@angular/core": "19.2.21", "@angular/platform-browser": "19.2.21", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-ktGPSjzWvSWMPtE8FyH9PTRZm0Qcjy+XOjAvGuj+Wdx4VBhdmEl4qa08Idd7MkWJH0n0LyPCupJdmcH0bsF1FA=="],
- "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+ "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
- "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+ "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
- "@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
+ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
- "@babel/generator": ["@babel/generator@7.26.10", "", { "dependencies": { "@babel/parser": "^7.26.10", "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang=="],
+ "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="],
- "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.29.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA=="],
@@ -206,13 +210,13 @@
"@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="],
- "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+ "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
- "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
- "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
@@ -226,17 +230,17 @@
"@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.24.7", "", { "dependencies": { "@babel/types": "^7.24.7" } }, "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA=="],
- "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
- "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.2", "", {}, "sha512-xExUBkuXWJjVuIbO7z6q7/BA9bgfJDEhVL0ggrggLMbg0IzCUWGT1hZGE8qUH7Il7/RD/a6cZ3AAFrrlp1LF/A=="],
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
- "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ=="],
- "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+ "@babel/helpers": ["@babel/helpers@7.29.10", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.8" } }, "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg=="],
- "@babel/parser": ["@babel/parser@8.0.0-rc.2", "", { "dependencies": { "@babel/types": "^8.0.0-rc.2" }, "bin": "./bin/babel-parser.js" }, "sha512-29AhEtcq4x8Dp3T72qvUMZHx0OMXCj4Jy/TEReQa+KWLln524Cj1fWb3QFi0l/xSpptQBR6y9RNEXuxpFvwiUQ=="],
+ "@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="],
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="],
@@ -366,11 +370,11 @@
"@babel/runtime": ["@babel/runtime@7.26.10", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="],
- "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
- "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+ "@babel/traverse": ["@babel/traverse@7.29.10", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ=="],
- "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@biomejs/biome": ["@biomejs/biome@2.4.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.0", "@biomejs/cli-darwin-x64": "2.4.0", "@biomejs/cli-linux-arm64": "2.4.0", "@biomejs/cli-linux-arm64-musl": "2.4.0", "@biomejs/cli-linux-x64": "2.4.0", "@biomejs/cli-linux-x64-musl": "2.4.0", "@biomejs/cli-win32-arm64": "2.4.0", "@biomejs/cli-win32-x64": "2.4.0" }, "bin": { "biome": "bin/biome" } }, "sha512-iluT61cORUDIC5i/y42ljyQraCemmmcgbMLLCnYO+yh+2hjTmcMFcwY8G0zTzWCsPb3t3AyKc+0t/VuhPZULUg=="],
@@ -436,12 +440,8 @@
"@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.6.3", "", {}, "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ=="],
- "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
-
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
- "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
-
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="],
@@ -494,7 +494,7 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="],
- "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.6.1", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.6.1" } }, "sha512-4Aji+soqukwUxq2DgHmkjxdGnG7hEiJuprqDlW4Wu6AQ0t8U9ItlICcM5to89pulIsEGrF1CkCoNrufQTcqb8A=="],
+ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@21.0.4", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^21.0.4" } }, "sha512-Bbr0hW9hhDhBf3Jp0EyDlkCbFgbr0z2jd3BMKYOYW+fOkPM4Nx195GMXyCtRCGEgoNn6i09FabXrSkNFm2ONzg=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
@@ -692,8 +692,6 @@
"@napi-rs/nice-win32-x64-msvc": ["@napi-rs/nice-win32-x64-msvc@1.1.1", "", { "os": "win32", "cpu": "x64" }, "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ=="],
- "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
-
"@next/env": ["@next/env@16.2.9", "", {}, "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw=="],
@@ -762,7 +760,7 @@
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-6gy4hhQSjq/T/S9hC9m3NxY0RY+9Ww+XNlB+8koIMTsMSYEjk7Ho+hFHQz1Bn4W61Ub7Vykufg+jgDgPfa2GFA=="],
- "@oxc-project/types": ["@oxc-project/types@0.112.0", "", {}, "sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ=="],
+ "@oxc-project/types": ["@oxc-project/types@0.154.0", "", {}, "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA=="],
"@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
@@ -796,43 +794,45 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+ "@playwright/test": ["@playwright/test@1.64.1", "", { "dependencies": { "playwright": "1.64.1" }, "bin": { "playwright": "cli.js" } }, "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg=="],
+
"@preact/signals": ["@preact/signals@1.3.4", "", { "dependencies": { "@preact/signals-core": "^1.7.0" }, "peerDependencies": { "preact": "10.x" } }, "sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g=="],
"@preact/signals-core": ["@preact/signals-core@1.14.1", "", {}, "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng=="],
"@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
- "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="],
+ "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.14", "", { "os": "android", "cpu": "arm" }, "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg=="],
- "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JWWLzvcmc/3pe7qdJqPpuPk91SoE/N+f3PcWx/6ZwuyDVyungAEJPvKm/eEldiDdwTmaEzWfIR+HORxYWrCi1A=="],
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.14", "", { "os": "android", "cpu": "arm64" }, "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ=="],
- "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-MTakBxfx3tde5WSmbHxuqlDsIW0EzQym+PJYGF4P6lG2NmKzi128OGynoFUqoD5ryCySEY85dug4v+LWGBElIw=="],
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw=="],
- "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jje3oopyOLs7IwfvXoS6Lxnmie5JJO7vW29fdGFu5YGY1EDbVDhD+P9vDihqS5X6fFiqL3ZQZCMBg6jyHkSVww=="],
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA=="],
- "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.3", "", { "os": "linux", "cpu": "arm" }, "sha512-A0n8P3hdLAaqzSFrQoA42p23ZKBYQOw+8EH5r15Sa9X1kD9/JXe0YT2gph2QTWvdr0CVK2BOXiK6ENfy6DXOag=="],
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA=="],
- "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kWXkoxxarYISBJ4bLNf5vFkEbb4JvccOwxWDxuK9yee8lg5XA7OpvlTptfRuwEvYcOZf+7VS69Uenpmpyo5Bjw=="],
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.14", "", { "os": "linux", "cpu": "arm" }, "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg=="],
- "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA=="],
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="],
- "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="],
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="],
- "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="],
- "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.3", "", { "os": "linux", "cpu": "x64" }, "sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ=="],
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="],
- "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.3", "", { "os": "linux", "cpu": "x64" }, "sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw=="],
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="],
- "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.3", "", { "os": "none", "cpu": "arm64" }, "sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ=="],
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="],
- "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.3", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-gekrQ3Q2HiC1T5njGyuUJoGpK/l6B/TNXKed3fZXNf9YRTJn3L5MOZsFBn4bN2+UX+8+7hgdlTcEsexX988G4g=="],
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.14", "", { "os": "none", "cpu": "arm64" }, "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q=="],
- "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-85y5JifyMgs8m5K2XzR/VDsapKbiFiohl7s5lEj7nmNGO0pkTXE7q6TQScei96BNAsoK7JC3pA7ukA8WRHVJpg=="],
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA=="],
- "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.3", "", { "os": "win32", "cpu": "x64" }, "sha512-a4VUQZH7LxGbUJ3qJ/TzQG8HxdHvf+jOnqf7B7oFx1TEBm+j2KNL2zr5SQ7wHkNAcaPevF6gf9tQnVBnC4mD+A=="],
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.14", "", { "os": "win32", "cpu": "x64" }, "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ=="],
- "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
@@ -944,8 +944,6 @@
"@tufjs/models": ["@tufjs/models@3.0.1", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^9.0.5" } }, "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA=="],
- "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
-
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -958,7 +956,7 @@
"@types/bonjour": ["@types/bonjour@3.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ=="],
- "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
+ "@types/bun": ["@types/bun@1.4.3", "", { "dependencies": { "bun-types": "1.4.3" } }, "sha512-N67Pqy8JXbAY/j0tNraS3V5mubGvc6rrrNDmDCtZLlFzccbUJPDb0tvgUBC5q4m8ybkP93Jcn1QB+RYQ6PvBhw=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
@@ -978,8 +976,6 @@
"@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="],
- "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="],
-
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
@@ -994,9 +990,9 @@
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
- "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+ "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
- "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+ "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
"@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="],
@@ -1016,6 +1012,46 @@
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
+ "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
+
+ "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
+
+ "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
+
+ "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
+
+ "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
+
+ "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
+
+ "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
+
+ "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
+
+ "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
+
+ "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
+
+ "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
+
+ "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
+
+ "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
+
+ "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
+
+ "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
+
+ "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
+
+ "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
+
+ "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
+
+ "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
+
+ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
+
"@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@1.2.0", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
@@ -1026,27 +1062,27 @@
"@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="],
- "@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
+ "@vue/compiler-core": ["@vue/compiler-core@3.5.44", "", { "dependencies": { "@babel/parser": "^7.29.9", "@vue/shared": "3.5.44", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.2" } }, "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ=="],
- "@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
+ "@vue/compiler-dom": ["@vue/compiler-dom@3.5.44", "", { "dependencies": { "@vue/compiler-core": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg=="],
- "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.28", "@vue/compiler-dom": "3.5.28", "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g=="],
+ "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.44", "", { "dependencies": { "@babel/parser": "^7.29.9", "@vue/compiler-core": "3.5.44", "@vue/compiler-dom": "3.5.44", "@vue/compiler-ssr": "3.5.44", "@vue/shared": "3.5.44", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.29", "source-map-js": "^1.2.2" } }, "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw=="],
- "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g=="],
+ "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.44", "", { "dependencies": { "@vue/compiler-dom": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA=="],
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
"@vue/language-core": ["@vue/language-core@2.2.12", "", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="],
- "@vue/reactivity": ["@vue/reactivity@3.5.28", "", { "dependencies": { "@vue/shared": "3.5.28" } }, "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw=="],
+ "@vue/reactivity": ["@vue/reactivity@3.5.44", "", { "dependencies": { "@vue/shared": "3.5.44" } }, "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw=="],
- "@vue/runtime-core": ["@vue/runtime-core@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ=="],
+ "@vue/runtime-core": ["@vue/runtime-core@3.5.44", "", { "dependencies": { "@vue/reactivity": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ=="],
- "@vue/runtime-dom": ["@vue/runtime-dom@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/runtime-core": "3.5.28", "@vue/shared": "3.5.28", "csstype": "^3.2.3" } }, "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA=="],
+ "@vue/runtime-dom": ["@vue/runtime-dom@3.5.44", "", { "dependencies": { "@vue/reactivity": "3.5.44", "@vue/runtime-core": "3.5.44", "@vue/shared": "3.5.44", "csstype": "^3.2.3" } }, "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg=="],
- "@vue/server-renderer": ["@vue/server-renderer@3.5.28", "", { "dependencies": { "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "vue": "3.5.28" } }, "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg=="],
+ "@vue/server-renderer": ["@vue/server-renderer@3.5.44", "", { "dependencies": { "@vue/compiler-ssr": "3.5.44", "@vue/runtime-dom": "3.5.44", "@vue/shared": "3.5.44" } }, "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ=="],
- "@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
+ "@vue/shared": ["@vue/shared@3.5.44", "", {}, "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg=="],
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
@@ -1084,6 +1120,32 @@
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
+ "@yuku-core/binding-android-arm64": ["@yuku-core/binding-android-arm64@0.19.0", "", { "os": "android", "cpu": "arm64" }, "sha512-wdL2XmTiZixAOaHk9Z0JJZ8u9Qm8d333Zjpc6gaxXFX+UR5BlsfxZ7a2+2Rm71pamppsPYGUGFq2oVLbh6DISw=="],
+
+ "@yuku-core/binding-darwin-arm64": ["@yuku-core/binding-darwin-arm64@0.19.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7sNJa6XJhyetnTy64jw3NEi4YgUcezHk1G5GH9FSvXAuC8wIlbaOwT9lgq5qo5sspjrZJQlBR1TWflP49uRpMw=="],
+
+ "@yuku-core/binding-darwin-x64": ["@yuku-core/binding-darwin-x64@0.19.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-pTC7dYEc59isdyizc3nsuyFNBu2LU5wMYZ1be6UMr59PY/AJNtO1aBLWV0MgL/z7C1nv/2sX4TNjGgKLoeevJw=="],
+
+ "@yuku-core/binding-freebsd-x64": ["@yuku-core/binding-freebsd-x64@0.19.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xrZjF6XOEwyLOiDIfrwDdRCS6ghxcJeirNC1KivQhVcUwlRonaU79S3/Uiyx3lBylPnT8qKJ3/aMT+H52nGTJQ=="],
+
+ "@yuku-core/binding-linux-arm-gnu": ["@yuku-core/binding-linux-arm-gnu@0.19.0", "", { "os": "linux", "cpu": "arm" }, "sha512-0OyIH53C0I1CbADHn9FSdCfX5qbpeLwJhmOVhonFJNYheybOnkSJ50qGMfTJIb8y5zrwqvLs7FVBvz6Fk8maZg=="],
+
+ "@yuku-core/binding-linux-arm-musl": ["@yuku-core/binding-linux-arm-musl@0.19.0", "", { "os": "linux", "cpu": "arm" }, "sha512-z/iPfkCOCSXE34VXNt0uP1ORSW+4mEa9b5CiJcWZeqZH08g49bFU/jRM2T04f+Wpw5mJ6HIzNBiojxeAbrHjeg=="],
+
+ "@yuku-core/binding-linux-arm64-gnu": ["@yuku-core/binding-linux-arm64-gnu@0.19.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jLNTi+gzaRwBJ+gwhCJ+rDb5dmx/8hEBx93UWd7hjMSqPTL1D1wvVSvXVAldV5kALvju7b0/m51EHEhs4+I1fA=="],
+
+ "@yuku-core/binding-linux-arm64-musl": ["@yuku-core/binding-linux-arm64-musl@0.19.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YwyPIdW/rN2Fmv0MTFWi47FaB3F9g+2b+sJISyAw83Yd60RWH4T3Nx6XkAHHVN+uKJNBAd6So7/BVihSiFYvuQ=="],
+
+ "@yuku-core/binding-linux-x64-gnu": ["@yuku-core/binding-linux-x64-gnu@0.19.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/vskaAXVX3ESR43FiTdfiYS7z1b9uoQJwVQ/0RgUHgN2sWoAyCZs+aX4CgU7eKGIHTVfp9VFwv+c+2XchYiEtQ=="],
+
+ "@yuku-core/binding-linux-x64-musl": ["@yuku-core/binding-linux-x64-musl@0.19.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FuXL5igeSKib9Rye02TdH+D6SQezTyGxIATMu1Qsn/QywrzwpGl35OLpS2C1x5vpGLeVE2XgiLd5mVPFHmIglQ=="],
+
+ "@yuku-core/binding-win32-arm64": ["@yuku-core/binding-win32-arm64@0.19.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YuJAe7zEKT1Xa7LuHlhGJpPnYQeVtvo0rZvNjJRqkH0hQGOw4C8QeP30Tg5VKGzZfo2x8CjhjEhKt4TmLwfmdg=="],
+
+ "@yuku-core/binding-win32-x64": ["@yuku-core/binding-win32-x64@0.19.0", "", { "os": "win32", "cpu": "x64" }, "sha512-o8iY/mXwlRkedp0LNTr4+TEfXTPQwoNZmv6k+1WIAd1ZXEn8xJJTBCtB0osaVvdZo/x/cNmV16UrWrrGpM5Mvw=="],
+
+ "@yuku-toolchain/types": ["@yuku-toolchain/types@0.19.0", "", {}, "sha512-tFkNbsP4WXX9UMUC5syJGdK9dPMLK0PPaIuXCcCYH75sZ8MrebfBZ2XvCILRTw8AgLivSSCfDa4R67iYBDa4Kg=="],
+
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
@@ -1114,8 +1176,6 @@
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
- "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
-
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -1126,8 +1186,6 @@
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
- "ast-kit": ["ast-kit@3.0.0-beta.1", "", { "dependencies": { "@babel/parser": "^8.0.0-beta.4", "estree-walker": "^3.0.3", "pathe": "^2.0.3" } }, "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw=="],
-
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -1164,8 +1222,6 @@
"bippy": ["bippy@0.3.34", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-vmptmU/20UdIWHHhq7qCSHhHzK7Ro3YJ1utU0fBG7ujUc58LEfTtilKxcF0IOgSjT5XLcm7CBzDjbv4lcKApGQ=="],
- "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
-
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="],
@@ -1184,17 +1240,19 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+ "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
+
"bun": ["bun@1.3.13", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.13", "@oven/bun-darwin-x64": "1.3.13", "@oven/bun-darwin-x64-baseline": "1.3.13", "@oven/bun-linux-aarch64": "1.3.13", "@oven/bun-linux-aarch64-musl": "1.3.13", "@oven/bun-linux-x64": "1.3.13", "@oven/bun-linux-x64-baseline": "1.3.13", "@oven/bun-linux-x64-musl": "1.3.13", "@oven/bun-linux-x64-musl-baseline": "1.3.13", "@oven/bun-windows-aarch64": "1.3.13", "@oven/bun-windows-x64": "1.3.13", "@oven/bun-windows-x64-baseline": "1.3.13" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-b9T4xZ8KqCHs4+TkHJv540LG1B8OD7noKu0Qaizusx3jFtMDHY6osNqgbaOlwW2B8RB2AKzz+sjzlGKIGxIjZw=="],
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
- "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
+ "bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
- "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
+ "cac": ["cac@7.1.0", "", {}, "sha512-LMDU/U2yA4IPIY8KT0NsNPWEYZvAMkbgyspxJ6btJaYV+WMDOHAZCmB0lsTObiHaRS1V/ztKAl7+TSkTqXyeoA=="],
"cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="],
@@ -1268,7 +1326,7 @@
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
- "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -1340,7 +1398,7 @@
"dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="],
- "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="],
+ "dts-resolver": ["dts-resolver@3.0.0", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -1354,7 +1412,7 @@
"emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="],
- "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
+ "empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
@@ -1470,7 +1528,7 @@
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
- "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
+ "get-tsconfig": ["get-tsconfig@5.0.0-beta.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw=="],
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
@@ -1488,7 +1546,7 @@
"handle-thing": ["handle-thing@2.0.1", "", {}, "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="],
- "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="],
+ "happy-dom": ["happy-dom@21.0.4", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-tFm+GaLBgo6o0awHnvHaNoraVo27qJ+2tZEtZ+cIa08hXbeitrxAJajLI05hsUy/2/YTKcUjQYzHe5mCcraLBw=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
@@ -1498,7 +1556,7 @@
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
- "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="],
+ "hookable": ["hookable@6.1.2", "", {}, "sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg=="],
"hosted-git-info": ["hosted-git-info@8.1.0", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw=="],
@@ -1544,7 +1602,7 @@
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
- "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="],
+ "import-without-cache": ["import-without-cache@0.4.1", "", {}, "sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -1810,7 +1868,7 @@
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
- "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+ "obug": ["obug@3.0.0", "", {}, "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -1872,11 +1930,9 @@
"path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="],
- "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
-
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
- "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
@@ -1884,9 +1940,9 @@
"pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="],
- "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
+ "playwright": ["playwright@1.64.1", "", { "dependencies": { "playwright-core": "1.64.1" }, "bin": { "playwright": "cli.js" } }, "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA=="],
- "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
+ "playwright-core": ["playwright-core@1.64.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ=="],
"postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="],
@@ -1936,9 +1992,11 @@
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
- "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+ "react": ["react@19.3.1", "", {}, "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw=="],
+
+ "react-compiler-runtime": ["react-compiler-runtime@1.0.0", "", { "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" } }, "sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w=="],
- "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+ "react-dom": ["react-dom@19.3.1", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.1" } }, "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ=="],
"react-scan": ["react-scan@0.4.3", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/generator": "^7.26.2", "@babel/types": "^7.26.0", "@clack/core": "^0.3.5", "@clack/prompts": "^0.8.2", "@pivanov/utils": "0.0.2", "@preact/signals": "^1.3.1", "@rollup/pluginutils": "^5.1.3", "@types/node": "^20.17.9", "bippy": "^0.3.8", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "kleur": "^4.1.5", "mri": "^1.2.0", "playwright": "^1.49.0", "preact": "^10.25.1", "tsx": "^4.19.3" }, "optionalDependencies": { "unplugin": "2.1.0" }, "peerDependencies": { "@remix-run/react": ">=1.0.0", "next": ">=13.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-router": "^5.0.0 || ^6.0.0 || ^7.0.0", "react-router-dom": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@remix-run/react", "next", "react-router", "react-router-dom"], "bin": { "react-scan": "bin/cli.js" } }, "sha512-jhAQuQ1nja6HUYrSpbmNFHqZPsRCXk8Yqu0lHoRIw9eb8N96uTfXCpVyQhTTnJ/nWqnwuvxbpKVG/oWZT8+iTQ=="],
@@ -1986,9 +2044,9 @@
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
- "rolldown": ["rolldown@1.0.0-rc.3", "", { "dependencies": { "@oxc-project/types": "=0.112.0", "@rolldown/pluginutils": "1.0.0-rc.3" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-x64": "1.0.0-rc.3", "@rolldown/binding-freebsd-x64": "1.0.0-rc.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.3", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.3", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.3", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.3", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.3", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.3" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Po/YZECDOqVXjIXrtC5h++a5NLvKAQNrd9ggrIG3sbDfGO5BqTUsrI6l8zdniKRp3r5Tp/2JTrXqx4GIguFCMw=="],
+ "rolldown": ["rolldown@1.2.14", "", { "dependencies": { "@oxc-project/types": "=0.154.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.14", "@rolldown/binding-android-arm64": "1.2.14", "@rolldown/binding-darwin-arm64": "1.2.14", "@rolldown/binding-darwin-x64": "1.2.14", "@rolldown/binding-freebsd-x64": "1.2.14", "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", "@rolldown/binding-linux-arm64-gnu": "1.2.14", "@rolldown/binding-linux-arm64-musl": "1.2.14", "@rolldown/binding-linux-ppc64-gnu": "1.2.14", "@rolldown/binding-linux-s390x-gnu": "1.2.14", "@rolldown/binding-linux-x64-gnu": "1.2.14", "@rolldown/binding-linux-x64-musl": "1.2.14", "@rolldown/binding-openharmony-arm64": "1.2.14", "@rolldown/binding-win32-arm64-msvc": "1.2.14", "@rolldown/binding-win32-x64-msvc": "1.2.14" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw=="],
- "rolldown-plugin-dts": ["rolldown-plugin-dts@0.22.5", "", { "dependencies": { "@babel/generator": "8.0.0-rc.2", "@babel/helper-validator-identifier": "8.0.0-rc.2", "@babel/parser": "8.0.0-rc.2", "@babel/types": "8.0.0-rc.2", "ast-kit": "^3.0.0-beta.1", "birpc": "^4.0.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.6", "obug": "^2.1.1" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-rc.3", "typescript": "^5.0.0 || ^6.0.0-beta", "vue-tsc": "~3.2.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-M/HXfM4cboo+jONx9Z0X+CUf3B5tCi7ni+kR5fUW50Fp9AlZk0oVLesibGWgCXDKFp5lpgQ9yhKoImUFjl3VZw=="],
+ "rolldown-plugin-dts": ["rolldown-plugin-dts@0.28.8", "", { "dependencies": { "dts-resolver": "^3.0.0", "get-tsconfig": "5.0.0-beta.6", "obug": "^3.0.0", "yuku-ast": "^0.19.0", "yuku-codegen": "^0.19.0", "yuku-parser": "^0.19.0" }, "peerDependencies": { "@typescript/native-preview": "*", "@volar/typescript": "~2.4.0", "@vue/language-core": "~3.2.0 || ~3.3.0", "rolldown": "^1.2.0", "typescript": "^5.0.0 || ^6.0.0 || ~7.0.0", "vue-tsc": "~3.2.0 || ~3.3.0" }, "optionalPeers": ["@typescript/native-preview", "@volar/typescript", "@vue/language-core", "typescript", "vue-tsc"] }, "sha512-FVhfqxorCWcc20BVT5eVRfRjzLLl6mgzjcMR4gSBf0K4oQMXT0qoHhhzfXy0L7y83fQy7Ug5ZDNk90xM3/LiCQ=="],
"rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="],
@@ -2010,7 +2068,7 @@
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
- "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+ "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
@@ -2024,9 +2082,9 @@
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
- "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="],
+ "seroval": ["seroval@1.6.11", "", {}, "sha512-hpb6XnMCGN/A+79Uf8V7/iTGkyyyazNn5X0CoCAp7Uy9vT3RT8jZ9GLcnxGoVjg3P8AArbe4ZVHq6C8oT2a0kA=="],
- "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="],
+ "seroval-plugins": ["seroval-plugins@1.6.11", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-LmB3nd8pL39jKHKvnpTUjcjBtNwR8MLty/ydAR8qbfXt5j6/6KqR1atuw8cVmD2/lDxAkvAwq5tI/HsisD9Fqg=="],
"serve-index": ["serve-index@1.9.2", "", { "dependencies": { "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.8.0", "mime-types": "~2.1.35", "parseurl": "~1.3.3" } }, "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ=="],
@@ -2070,13 +2128,13 @@
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
- "solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
+ "solid-js": ["solid-js@1.9.17", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.6.8", "seroval-plugins": "~1.6.8" } }, "sha512-4GAmLOWPfNV8H9Jfn2LSFTpNEG6jiOKPiwd5lnLGQtehHaX6l3TOLzJqQ7mqwB/3k1Yy3zyumFVopmc/tm34ow=="],
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
- "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+ "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="],
"source-map-loader": ["source-map-loader@5.0.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, "peerDependencies": { "webpack": "^5.72.1" } }, "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA=="],
@@ -2142,9 +2200,9 @@
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
- "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
+ "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="],
- "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@@ -2156,7 +2214,7 @@
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
- "tsdown": ["tsdown@0.20.3", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "defu": "^6.1.4", "empathic": "^2.0.0", "hookable": "^6.0.1", "import-without-cache": "^0.2.5", "obug": "^2.1.1", "picomatch": "^4.0.3", "rolldown": "1.0.0-rc.3", "rolldown-plugin-dts": "^0.22.1", "semver": "^7.7.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig-core": "^7.4.2", "unrun": "^0.2.27" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@vitejs/devtools": "*", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "@vitejs/devtools", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-qWOUXSbe4jN8JZEgrkc/uhJpC8VN2QpNu3eZkBWwNuTEjc/Ik1kcc54ycfcQ5QPRHeu9OQXaLfCI3o7pEJgB2w=="],
+ "tsdown": ["tsdown@0.23.1", "", { "dependencies": { "cac": "^7.0.1", "defu": "^6.1.7", "empathic": "^2.1.0", "hookable": "^6.1.2", "import-without-cache": "^0.4.1", "obug": "^3.0.0", "picomatch": "^4.0.7", "rolldown": "~1.2.13", "rolldown-plugin-dts": "^0.28.7", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "tree-kill": "^1.2.2", "unconfig-core": "^7.5.0", "verkit": "^0.5.0" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.23.1", "@tsdown/exe": "0.23.1", "@vitejs/devtools": "*", "publint": "^0.3.8", "tsx": "*", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": ">=0.5.0", "unrun": "*" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@vitejs/devtools", "publint", "tsx", "typescript", "unplugin-unused", "unrun"], "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-MSC2WSOts3in95J334/VIS7KPfor3eCSvndJ6KdoVj9Aak9Kh1aNfz9ws5owhdukuTNSXRdfkPEzA/ihpDD6SA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -2168,7 +2226,7 @@
"typed-assert": ["typed-assert@1.0.9", "", {}, "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg=="],
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+ "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
"unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="],
@@ -2194,8 +2252,6 @@
"unplugin": ["unplugin@2.1.0", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ=="],
- "unrun": ["unrun@0.2.37", "", { "dependencies": { "rolldown": "1.0.0-rc.17" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-AA7vDuYsgeSYVzJMm16UKA+aXFKhy7nFqW9z5l7q44K4ppFWZAMqYS58ePRZbugMLPH0fwwMzD5A8nP0avxwZQ=="],
-
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@@ -2210,6 +2266,8 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+ "verkit": ["verkit@0.5.0", "", {}, "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g=="],
+
"vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
@@ -2218,7 +2276,7 @@
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
- "vue": ["vue@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", "@vue/runtime-dom": "3.5.28", "@vue/server-renderer": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg=="],
+ "vue": ["vue@3.5.44", "", { "dependencies": { "@vue/compiler-dom": "3.5.44", "@vue/compiler-sfc": "3.5.44", "@vue/runtime-dom": "3.5.44", "@vue/server-renderer": "3.5.44", "@vue/shared": "3.5.44" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ=="],
"vue-tsc": ["vue-tsc@2.2.12", "", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="],
@@ -2262,11 +2320,11 @@
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
- "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
+ "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
- "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -2276,12 +2334,26 @@
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
+ "yuku-ast": ["yuku-ast@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0" } }, "sha512-PyCsiScaelhxBGzL8dyeJkLqvRZycoh78R31Vrt/WqYrm0hMFoKwIw/X7Dx5mBKLWAMlV9I9HEyBOHnj2gZVyw=="],
+
+ "yuku-codegen": ["yuku-codegen@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0" } }, "sha512-uymAKHjDuaP4BEpkXC/BSLz4DUEF5oArZ5AuD3888nb2miChjWSgpPdojEJxm3pjpXkgUaCRJQyNAbnMN33wHg=="],
+
+ "yuku-core": ["yuku-core@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0" }, "optionalDependencies": { "@yuku-core/binding-android-arm64": "0.19.0", "@yuku-core/binding-darwin-arm64": "0.19.0", "@yuku-core/binding-darwin-x64": "0.19.0", "@yuku-core/binding-freebsd-x64": "0.19.0", "@yuku-core/binding-linux-arm-gnu": "0.19.0", "@yuku-core/binding-linux-arm-musl": "0.19.0", "@yuku-core/binding-linux-arm64-gnu": "0.19.0", "@yuku-core/binding-linux-arm64-musl": "0.19.0", "@yuku-core/binding-linux-x64-gnu": "0.19.0", "@yuku-core/binding-linux-x64-musl": "0.19.0", "@yuku-core/binding-win32-arm64": "0.19.0", "@yuku-core/binding-win32-x64": "0.19.0" } }, "sha512-kYK9e1XdfirNhkipzW0wmMcSWcVF/CWnuugodfVYRCUwvoBefweL9HzB96OiKEhsx78V5WbUxbJqlizMX869Qg=="],
+
+ "yuku-parser": ["yuku-parser@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0", "yuku-core": "0.19.0" } }, "sha512-XrBh9ZiQK5mJVRxZBAvD6LwRyAYrK3MFtH3+onyuxFL5hFD44EC3QOmIC1FjTj6H2MMwq46/NAI+NZxLZPZgxg=="],
+
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"zone.js": ["zone.js@0.15.1", "", {}, "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w=="],
"@angular-devkit/architect/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
+ "@angular-devkit/build-angular/@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
+
+ "@angular-devkit/build-angular/@babel/generator": ["@babel/generator@7.26.10", "", { "dependencies": { "@babel/parser": "^7.26.10", "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang=="],
+
+ "@angular-devkit/build-angular/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@angular-devkit/build-angular/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"@angular-devkit/build-angular/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@@ -2290,6 +2362,8 @@
"@angular-devkit/build-webpack/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
+ "@angular-devkit/core/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"@angular-devkit/schematics/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
@@ -2298,8 +2372,12 @@
"@angular/animations/@angular/core": ["@angular/core@19.2.21", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" } }, "sha512-jdOEwkvUyF7VdJMURXkSB1yQy595wt1lSfSgUux9tZftA7UwamCsMdidUsKqRbdaI9M3r+RdIM7qrDAWwqzz+A=="],
+ "@angular/build/@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
+
"@angular/build/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+ "@angular/build/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@angular/build/rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"@angular/build/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@@ -2312,6 +2390,8 @@
"@angular/compiler-cli/@babel/core": ["@babel/core@7.26.9", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.9", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.9", "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw=="],
+ "@angular/compiler-cli/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
+
"@angular/compiler-cli/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"@angular/forms/@angular/core": ["@angular/core@19.2.21", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" } }, "sha512-jdOEwkvUyF7VdJMURXkSB1yQy595wt1lSfSgUux9tZftA7UwamCsMdidUsKqRbdaI9M3r+RdIM7qrDAWwqzz+A=="],
@@ -2322,55 +2402,107 @@
"@angular/router/@angular/core": ["@angular/core@19.2.21", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" } }, "sha512-jdOEwkvUyF7VdJMURXkSB1yQy595wt1lSfSgUux9tZftA7UwamCsMdidUsKqRbdaI9M3r+RdIM7qrDAWwqzz+A=="],
- "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
-
- "@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
-
- "@babel/core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
-
- "@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
-
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "@babel/generator/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
+ "@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
"@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
"@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
- "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+ "@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
- "@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.2", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.2", "@babel/helper-validator-identifier": "^8.0.0-rc.2" } }, "sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw=="],
+ "@babel/helper-remap-async-to-generator/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-split-export-declaration/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-wrap-function/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/helper-wrap-function/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/helper-wrap-function/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
+ "@babel/plugin-transform-classes/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/plugin-transform-classes/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-computed-properties/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-function-name/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
"@babel/plugin-transform-modules-systemjs/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
"@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
"@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "@babel/preset-env/@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw=="],
+ "@babel/preset-env/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
- "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "@babel/preset-env/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
- "@babel/template/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "@babel/preset-env/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
- "@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+ "@babel/preset-env/@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw=="],
- "@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+ "@babel/preset-modules/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@inquirer/checkbox/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
@@ -2438,12 +2570,18 @@
"@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+ "@parcel/watcher/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+ "@rollup/pluginutils/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+ "@tailwindcss/node/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -2460,17 +2598,25 @@
"@types/babel__core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "@types/babel__core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@types/babel__generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"@types/babel__template/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
- "@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "@types/babel__template/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
- "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+ "@types/babel__traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
- "@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
- "@vue/compiler-sfc/postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="],
+ "@vue/compiler-sfc/postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="],
+
+ "@vue/language-core/@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
+
+ "@vue/language-core/@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
@@ -2480,8 +2626,14 @@
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
+ "babel-plugin-jsx-dom-expressions/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "babel-plugin-polyfill-corejs2/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "babel-plugin-react-compiler/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"beasties/postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -2504,10 +2656,24 @@
"classy-store-nextjs-example/react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
- "classy-store-react-example/@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
+ "classy-store-nextjs-example/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "classy-store-react-example/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "classy-store-react-example/@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "classy-store-react-example/react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "classy-store-react-example/react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"classy-store-react-example/tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
+ "classy-store-solid-example/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "classy-store-svelte-example/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "classy-store-vue-example/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -2558,8 +2724,6 @@
"log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
- "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
-
"make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
@@ -2578,8 +2742,12 @@
"node-gyp/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="],
+ "node-gyp/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
+ "parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"parse5-html-rewriting-stream/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -2588,7 +2756,7 @@
"pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="],
- "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
+ "postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
@@ -2600,10 +2768,16 @@
"react-scan/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+ "react-scan/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"react-scan/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+ "react-scan/playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
+
"read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
+ "resolve-url-loader/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
+
"resolve-url-loader/loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
"resolve-url-loader/postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="],
@@ -2612,9 +2786,7 @@
"restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
- "rolldown-plugin-dts/@babel/generator": ["@babel/generator@8.0.0-rc.2", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.2", "@babel/types": "^8.0.0-rc.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-oCQ1IKPwkzCeJzAPb7Fv8rQ9k5+1sG8mf2uoHiMInPYvkRfrDJxbTIbH51U+jstlkghus0vAi3EBvkfvEsYNLQ=="],
-
- "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.2", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.2", "@babel/helper-validator-identifier": "^8.0.0-rc.2" } }, "sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw=="],
+ "sass/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
@@ -2630,8 +2802,14 @@
"solid-refresh/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+ "solid-refresh/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "solid-refresh/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"source-map-loader/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+ "source-map-loader/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
@@ -2640,16 +2818,22 @@
"tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
+ "tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
"tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
- "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
+ "tsx/get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
- "unrun/rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
+ "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
"vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+ "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"vite/postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="],
+ "vite/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"vite-plugin-solid/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"webpack/watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
@@ -2658,12 +2842,58 @@
"webpack-dev-server/http-proxy-middleware": ["http-proxy-middleware@2.0.9", "", { "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" }, "peerDependencies": { "@types/express": "^4.17.13" }, "optionalPeers": ["@types/express"] }, "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q=="],
+ "webpack-dev-server/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
+
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+ "@angular-devkit/build-angular/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@angular-devkit/build-angular/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@angular-devkit/build-angular/@babel/generator/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@angular-devkit/build-angular/@babel/generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@angular/build/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@angular/build/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@angular/build/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@angular/build/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@angular/build/@babel/core/@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
+ "@angular/build/@babel/core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@angular/build/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@angular/build/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@angular/build/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@angular/build/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
"@angular/build/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@angular/build/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
@@ -2714,15 +2944,285 @@
"@angular/build/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+ "@angular/compiler-cli/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
"@angular/compiler-cli/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+ "@angular/compiler-cli/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
"@angular/compiler-cli/@babel/core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "@angular/compiler-cli/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"@angular/compiler-cli/@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"@angular/compiler-cli/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.4", "", {}, "sha512-dluR3v287dp6YPF57kyKKrHPKffUeuxH1zQcF1WD30TeFzWXhDiVi1U6PkqaDB0++H1PeCwRhmYl4DvoerlPIw=="],
+ "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-split-export-declaration/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-split-export-declaration/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-wrap-function/@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-wrap-function/@babel/template/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/helper-wrap-function/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-wrap-function/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-wrap-function/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-wrap-function/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/helper-wrap-function/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-wrap-function/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-classes/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-classes/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "@babel/plugin-transform-classes/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/plugin-transform-classes/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-computed-properties/@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-computed-properties/@babel/template/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-computed-properties/@babel/template/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/preset-modules/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/preset-modules/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@inquirer/prompts/@inquirer/confirm/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
@@ -2746,13 +3246,47 @@
"@npmcli/run-script/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
+ "@tailwindcss/postcss/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@types/babel__generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@types/babel__template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@types/babel__traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="],
+
+ "@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
+
+ "babel-plugin-jsx-dom-expressions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "babel-plugin-jsx-dom-expressions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "babel-plugin-react-compiler/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "babel-plugin-react-compiler/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "beasties/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"cacache/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"cacache/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
- "classy-store-react-example/@types/bun/bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
+ "classy-store-nextjs-example/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "classy-store-react-example/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -2764,6 +3298,8 @@
"copy-webpack-plugin/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
+ "css-loader/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -2772,12 +3308,26 @@
"hpack.js/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
+ "istanbul-lib-instrument/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
"istanbul-lib-instrument/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
- "istanbul-lib-instrument/@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+ "istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"istanbul-lib-instrument/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "istanbul-lib-instrument/@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -2786,24 +3336,50 @@
"log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+ "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+ "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+ "next/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"node-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"node-gyp/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
+ "node-gyp/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
+ "parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
"pkg-dir/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="],
"pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
+ "react-scan/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "react-scan/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "react-scan/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "react-scan/@babel/core/@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
"react-scan/@babel/core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
- "react-scan/@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+ "react-scan/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "react-scan/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"react-scan/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"react-scan/@babel/generator/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "react-scan/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "react-scan/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
"react-scan/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"react-scan/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
@@ -2856,9 +3432,13 @@
"react-scan/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+ "react-scan/playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
+
+ "react-scan/playwright/playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
+
"read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
- "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.4", "", {}, "sha512-dluR3v287dp6YPF57kyKKrHPKffUeuxH1zQcF1WD30TeFzWXhDiVi1U6PkqaDB0++H1PeCwRhmYl4DvoerlPIw=="],
+ "resolve-url-loader/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -2870,6 +3450,12 @@
"solid-refresh/@babel/generator/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+ "solid-refresh/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "solid-refresh/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "solid-refresh/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
"tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -2926,41 +3512,23 @@
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
- "unrun/rolldown/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
-
- "unrun/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
-
- "unrun/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
-
- "unrun/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
+ "vite-plugin-solid/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
- "unrun/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
-
- "unrun/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
-
- "unrun/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="],
-
- "unrun/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
-
- "unrun/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
+ "vite-plugin-solid/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
- "unrun/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
+ "vite-plugin-solid/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
- "unrun/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
+ "vite-plugin-solid/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
- "unrun/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="],
+ "vite-plugin-solid/@babel/core/@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
- "unrun/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
+ "vite-plugin-solid/@babel/core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
- "unrun/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
+ "vite-plugin-solid/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
- "unrun/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
+ "vite-plugin-solid/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
- "vite-plugin-solid/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
-
- "vite-plugin-solid/@babel/core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
-
- "vite-plugin-solid/@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+ "vite-plugin-solid/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"vite-plugin-solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -3016,6 +3584,8 @@
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+ "vite/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"webpack-dev-server/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"webpack-dev-server/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
@@ -3024,14 +3594,322 @@
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "@angular-devkit/build-angular/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@angular-devkit/build-angular/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular-devkit/build-angular/@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@angular-devkit/build-angular/@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular/build/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular/build/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "@angular/build/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@angular/build/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@angular/build/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular/build/@babel/core/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@angular/build/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@angular/build/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@angular/compiler-cli/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-wrap-function/@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-wrap-function/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-classes/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-classes/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-classes/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-computed-properties/@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-computed-properties/@babel/template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-computed-properties/@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-function-name/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+
+ "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "istanbul-lib-instrument/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "istanbul-lib-instrument/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "istanbul-lib-instrument/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "istanbul-lib-instrument/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
"log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
+ "react-scan/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "react-scan/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "react-scan/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "react-scan/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "react-scan/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "react-scan/@babel/core/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
+ "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "vite-plugin-solid/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "vite-plugin-solid/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
+
+ "vite-plugin-solid/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "vite-plugin-solid/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "vite-plugin-solid/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "vite-plugin-solid/@babel/core/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "vite-plugin-solid/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "vite-plugin-solid/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
"webpack-dev-server/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+ "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-modules-amd/@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-modules-systemjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-modules-umd/@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
+
+ "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
}
}
diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx
index b2d21ed..16c7ecb 100644
--- a/examples/react/src/App.tsx
+++ b/examples/react/src/App.tsx
@@ -12,6 +12,7 @@ import {ReactivityPage} from './pages/ReactivityPage';
import {ShallowEqualPage} from './pages/ShallowEqualPage';
import {SnapshotsPage} from './pages/SnapshotsPage';
import {SubscribeKeyPage} from './pages/SubscribeKeyPage';
+import {ScopedStoresPage} from './pages/scopedStores/ScopedStoresPage';
import {UseLocalStorePage} from './pages/UseLocalStorePage';
const routes: Record = {
@@ -20,6 +21,7 @@ const routes: Record = {
'/collections': CollectionsPage,
'/snapshots': SnapshotsPage,
'/use-local-store': UseLocalStorePage,
+ '/scoped-stores': ScopedStoresPage,
'/persist': PersistPage,
'/history': HistoryPage,
'/devtools': DevtoolsPage,
diff --git a/examples/react/src/components/NavSidebar.tsx b/examples/react/src/components/NavSidebar.tsx
index c50f1ec..52114ef 100644
--- a/examples/react/src/components/NavSidebar.tsx
+++ b/examples/react/src/components/NavSidebar.tsx
@@ -19,6 +19,11 @@ const navGroups = [
label: 'useLocalStore',
route: '/use-local-store',
},
+ {
+ href: '#/scoped-stores',
+ label: 'Scoped stores',
+ route: '/scoped-stores',
+ },
],
},
{
diff --git a/examples/react/src/demos/reactivity/ReactiveFundamentalsDemo.tsx b/examples/react/src/demos/reactivity/ReactiveFundamentalsDemo.tsx
index a755b8d..518b15c 100644
--- a/examples/react/src/demos/reactivity/ReactiveFundamentalsDemo.tsx
+++ b/examples/react/src/demos/reactivity/ReactiveFundamentalsDemo.tsx
@@ -55,13 +55,13 @@ function NameCard() {
}
function BatchCard() {
- // 1000 mutations β 1 render (microtask batching)
+ // 1000 mutations β 1 render in this event (React batching)
const count = useClassyStore(batchStore, (state) => state.count);
return {count}
;
}
function NameInput() {
- const name = useClassyStore(formStore, (state) => state.name, { sync: true });
+ const name = useClassyStore(formStore, (state) => state.name);
return (
state.name, {sync: true});
+ const name = useClassyStore(formStore, (state) => state.name);
const renders = useRenderCount();
return (
@@ -152,7 +152,7 @@ export function ReactiveFundamentalsDemo() {
return (
+
+ {label} title
+ store.setTitle(event.target.value)}
+ />
+
+
+ {label} body
+
+
+ );
+}
diff --git a/examples/react/src/demos/scoped-store/components/editorPreview/EditorPreview.tsx b/examples/react/src/demos/scoped-store/components/editorPreview/EditorPreview.tsx
new file mode 100644
index 0000000..1734972
--- /dev/null
+++ b/examples/react/src/demos/scoped-store/components/editorPreview/EditorPreview.tsx
@@ -0,0 +1,17 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useEditorStore} from '../../store/hooks/useEditorStore';
+
+type Props = {label: string};
+
+export function EditorPreview({label}: Props) {
+ const store = useEditorStore();
+ const title = useClassyStore(store, (state) => state.title);
+ return (
+
+ Shared preview: {title || 'Untitled'}
+
+ );
+}
diff --git a/examples/react/src/demos/scoped-store/components/editorSession/EditorSession.tsx b/examples/react/src/demos/scoped-store/components/editorSession/EditorSession.tsx
new file mode 100644
index 0000000..3df149a
--- /dev/null
+++ b/examples/react/src/demos/scoped-store/components/editorSession/EditorSession.tsx
@@ -0,0 +1,39 @@
+import {type ReactNode, useState} from 'react';
+import {Button} from '../../../../components/Button';
+import {EditorStoreProvider} from '../../store/components/editorStoreProvider/EditorStoreProvider';
+import type {EditorData} from '../../store/editorStore';
+import {EditorFields} from '../editorFields/EditorFields';
+import {EditorPreview} from '../editorPreview/EditorPreview';
+
+type Props = {
+ label: string;
+ initialData: EditorData;
+ children?: ReactNode;
+};
+
+export function EditorSession({label, initialData, children}: Props) {
+ const [generation, setGeneration] = useState(0);
+ return (
+
+
+
{label}
+ setGeneration((previous) => previous + 1)}
+ >
+ Reset
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/examples/react/src/demos/scoped-store/components/scopedStoresDemo/ScopedStoresDemo.tsx b/examples/react/src/demos/scoped-store/components/scopedStoresDemo/ScopedStoresDemo.tsx
new file mode 100644
index 0000000..b632c20
--- /dev/null
+++ b/examples/react/src/demos/scoped-store/components/scopedStoresDemo/ScopedStoresDemo.tsx
@@ -0,0 +1,64 @@
+import {useState} from 'react';
+import {Button} from '../../../../components/Button';
+import {EditorSession} from '../editorSession/EditorSession';
+
+export function ScopedStoresDemo() {
+ const [refreshes, setRefreshes] = useState(0);
+ const [showRight, setShowRight] = useState(true);
+
+ return (
+
+
+ setRefreshes((previous) => previous + 1)}
+ >
+ Re-render parent ({refreshes})
+
+ setShowRight((previous) => !previous)}
+ >
+ {showRight ? 'Unmount right editor' : 'Mount right editor'}
+
+
+
+ Edits stay in their scope. Refreshing the parent preserves drafts; reset
+ starts a new session. Unmounting and mounting the right editor creates a
+ fresh draft.
+
+
+
+
+
+ This nested editor has its own provider. Resetting the left scope
+ also remounts its nested scope.
+
+
+
+
+ {showRight && (
+
+ )}
+
+
+ );
+}
diff --git a/examples/react/src/demos/scoped-store/store/components/editorStoreProvider/EditorStoreProvider.tsx b/examples/react/src/demos/scoped-store/store/components/editorStoreProvider/EditorStoreProvider.tsx
new file mode 100644
index 0000000..0a1b457
--- /dev/null
+++ b/examples/react/src/demos/scoped-store/store/components/editorStoreProvider/EditorStoreProvider.tsx
@@ -0,0 +1,45 @@
+import {
+ prepareStoreForHydration,
+ useLocalStore,
+} from '@codebelt/classy-store/react';
+import {persist, type StorageAdapter} from '@codebelt/classy-store/utils';
+import {type ReactNode, useEffect} from 'react';
+import {type EditorData, EditorStore} from '../../editorStore';
+import {EditorStoreContext} from '../../editorStoreContext';
+
+type Props = {
+ initialData: EditorData;
+ children: ReactNode;
+ persistence?: {name: string; storage: StorageAdapter};
+};
+
+/** Own one store per mount. Change the provider's key to start a new session. */
+export function EditorStoreProvider({
+ initialData,
+ children,
+ persistence,
+}: Props) {
+ const store = prepareStoreForHydration(
+ useLocalStore(() => new EditorStore(initialData)),
+ );
+ const name = persistence?.name;
+ const storage = persistence?.storage;
+
+ useEffect(() => {
+ if (name === undefined || storage === undefined) return;
+ const handle = persist(store, {
+ name,
+ storage,
+ properties: ['title', 'body'],
+ skipHydration: true,
+ });
+ void handle.rehydrate();
+ return () => handle.unsubscribe();
+ }, [store, name, storage]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/examples/react/src/demos/scoped-store/store/editorStore.ts b/examples/react/src/demos/scoped-store/store/editorStore.ts
new file mode 100644
index 0000000..0866529
--- /dev/null
+++ b/examples/react/src/demos/scoped-store/store/editorStore.ts
@@ -0,0 +1,22 @@
+export type EditorData = {
+ title: string;
+ body: string;
+};
+
+export class EditorStore {
+ title: string;
+ body: string;
+
+ constructor(initialData: EditorData) {
+ this.title = initialData.title;
+ this.body = initialData.body;
+ }
+
+ setTitle(title: string) {
+ this.title = title;
+ }
+
+ setBody(body: string) {
+ this.body = body;
+ }
+}
diff --git a/examples/react/src/demos/scoped-store/store/editorStoreContext.ts b/examples/react/src/demos/scoped-store/store/editorStoreContext.ts
new file mode 100644
index 0000000..2642caa
--- /dev/null
+++ b/examples/react/src/demos/scoped-store/store/editorStoreContext.ts
@@ -0,0 +1,5 @@
+import {createContext} from 'react';
+import type {EditorStore} from './editorStore';
+
+// Share the context definition, never a mutable store instance, across requests.
+export const EditorStoreContext = createContext(null);
diff --git a/examples/react/src/demos/scoped-store/store/hooks/useEditorStore.ts b/examples/react/src/demos/scoped-store/store/hooks/useEditorStore.ts
new file mode 100644
index 0000000..f0e6951
--- /dev/null
+++ b/examples/react/src/demos/scoped-store/store/hooks/useEditorStore.ts
@@ -0,0 +1,11 @@
+import {useContext} from 'react';
+import {EditorStoreContext} from '../editorStoreContext';
+
+/** Get the nearest scope's mutable store; useClassyStore subscribes to its data. */
+export function useEditorStore() {
+ const store = useContext(EditorStoreContext);
+ if (!store) {
+ throw new Error('useEditorStore must be used within EditorStoreProvider');
+ }
+ return store;
+}
diff --git a/examples/react/src/pages/ReactivityPage.tsx b/examples/react/src/pages/ReactivityPage.tsx
index 1173bb8..4c15a5e 100644
--- a/examples/react/src/pages/ReactivityPage.tsx
+++ b/examples/react/src/pages/ReactivityPage.tsx
@@ -8,7 +8,7 @@ export function ReactivityPage() {
Reactivity
- Selectors, microtask batching, and async data fetching β watch the
+ Selectors, React render batching, and async data fetching β watch the
render badges to see exactly which components re-render.
diff --git a/examples/react/src/pages/scopedStores/ScopedStoresPage.tsx b/examples/react/src/pages/scopedStores/ScopedStoresPage.tsx
new file mode 100644
index 0000000..9b7576b
--- /dev/null
+++ b/examples/react/src/pages/scopedStores/ScopedStoresPage.tsx
@@ -0,0 +1,23 @@
+import {TipBox} from '../../components/TipBox';
+import {ScopedStoresDemo} from '../../demos/scoped-store/components/scopedStoresDemo/ScopedStoresDemo';
+
+export function ScopedStoresPage() {
+ return (
+
+
+
+
+ Context chooses the store instance; useClassyStore subscribes to its
+ values. The provider owns creation, hydration preparation, and optional
+ persistence cleanup. Changing initialData alone preserves the current
+ draft; change the provider key to reset it.
+
+
+ );
+}
diff --git a/examples/react/tsconfig.scoping.json b/examples/react/tsconfig.scoping.json
new file mode 100644
index 0000000..2b24eba
--- /dev/null
+++ b/examples/react/tsconfig.scoping.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "paths": {
+ "@codebelt/classy-store": ["../../packages/classy-store/src/index.ts"],
+ "@codebelt/classy-store/react": [
+ "../../packages/classy-store/src/frameworks/react/react.ts"
+ ],
+ "@codebelt/classy-store/utils": [
+ "../../packages/classy-store/src/utils/index.ts"
+ ]
+ }
+ },
+ "include": [
+ "src/demos/scoped-store/**/*.ts",
+ "src/demos/scoped-store/**/*.tsx",
+ "src/pages/scopedStores/**/*.tsx"
+ ]
+}
diff --git a/package.json b/package.json
index 08bb5ef..1b70f6d 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,16 @@
"build": "bun run --filter @codebelt/classy-store build",
"---------- Test --------------------------------------------------------": "",
"test": "bun run --filter @codebelt/classy-store test",
+ "test:browser": "bun run --filter @codebelt/classy-store test:browser",
+ "test:react:compatibility": "bun run build && node packages/classy-store/tests/compatibility/runCompatibility.mjs",
+ "test:react:compiler": "bun run build && node packages/classy-store/tests/compiler/runCompiler.mjs",
+ "test:react:memory": "bun run build && bun packages/classy-store/tests/memory/runMemory.ts",
+ "test:react:next": "bun run build && node packages/classy-store/tests/next/runNext.mjs",
+ "benchmark:react": "bun run build && bun packages/classy-store/tests/performance/runPerformance.ts",
"---------- Lint --------------------------------------------------------": "",
"lint": "biome check",
"lint:fix": "biome check --write",
- "typecheck": "bun run --filter @codebelt/classy-store typecheck",
+ "typecheck": "bun run --filter @codebelt/classy-store typecheck && bun run --filter @codebelt/classy-store typecheck:scoping",
"---------- Release (Changesets) ----------------------------------------": "",
"changeset": "changeset",
"changeset:add": "changeset add",
diff --git a/packages/classy-store/package.json b/packages/classy-store/package.json
index 5fe8f28..582f49d 100644
--- a/packages/classy-store/package.json
+++ b/packages/classy-store/package.json
@@ -56,6 +56,16 @@
"default": "./dist/frameworks/react/react.cjs"
}
},
+ "./react/server": {
+ "import": {
+ "types": "./dist/frameworks/react/server.d.mts",
+ "default": "./dist/frameworks/react/server.mjs"
+ },
+ "require": {
+ "types": "./dist/frameworks/react/server.d.cts",
+ "default": "./dist/frameworks/react/server.cjs"
+ }
+ },
"./vue": {
"import": {
"types": "./dist/frameworks/vue/vue.d.mts",
@@ -107,9 +117,11 @@
"---------- Build ------------------------------------------------------": "",
"build": "tsdown",
"---------- Test --------------------------------------------------------": "",
- "test": "bun test",
+ "test": "bun test src tests/memory/heapSnapshot.test.ts",
+ "test:browser": "playwright test --config tests/browser/playwright.config.ts",
"---------- Lint --------------------------------------------------------": "",
- "typecheck": "tsc --noEmit",
+ "typecheck": "tsc --noEmit && tsc --project tests/browser/tsconfig.json --noEmit && tsc --project tests/compiler/tsconfig.json --noEmit && tsc --project tests/performance/tsconfig.json --noEmit && tsc --project tests/memory/tsconfig.json --noEmit && tsc --project tests/next/tsconfig.json --noEmit",
+ "typecheck:scoping": "tsc --project ../../examples/react/tsconfig.scoping.json --noEmit",
"prepublishOnly": "cp ../../README.md .",
"-----------------------------------------------------------------------": ""
},
@@ -141,17 +153,21 @@
}
},
"devDependencies": {
- "@angular/core": "21.1.4",
- "@happy-dom/global-registrator": "20.6.1",
- "@types/bun": "1.3.9",
- "@types/react": "19.2.14",
- "@types/react-dom": "19.2.3",
- "react": "19.2.4",
- "react-dom": "19.2.4",
- "solid-js": "1.9.11",
- "tsdown": "0.20.3",
- "typescript": "5.9.3",
- "vue": "3.5.28"
+ "@angular/core": "22.2.3",
+ "@babel/core": "7.29.7",
+ "@happy-dom/global-registrator": "21.0.4",
+ "@playwright/test": "1.64.1",
+ "@types/bun": "1.4.3",
+ "@types/react": "19.3.0",
+ "@types/react-dom": "19.3.0",
+ "babel-plugin-react-compiler": "1.0.0",
+ "react": "19.3.1",
+ "react-compiler-runtime": "1.0.0",
+ "react-dom": "19.3.1",
+ "solid-js": "1.9.17",
+ "tsdown": "0.23.1",
+ "typescript": "7.0.2",
+ "vue": "3.5.44"
},
"keywords": [
"classy-store",
diff --git a/packages/classy-store/src/core/core.memory.qa.test.ts b/packages/classy-store/src/core/core.memory.qa.test.ts
new file mode 100644
index 0000000..a1c55c7
--- /dev/null
+++ b/packages/classy-store/src/core/core.memory.qa.test.ts
@@ -0,0 +1,148 @@
+import {describe, expect, test} from 'bun:test';
+import {createClassyStore, getInternal, subscribe} from './core';
+
+describe('createClassyStore memory ownership QA', () => {
+ test.each([
+ {description: 'replacement', remove: 'replace'},
+ {description: 'deletion', remove: 'delete'},
+ ])('disconnects retired subtrees after $description', ({remove}) => {
+ const store = createClassyStore<{section?: {row: {value: number}}}>({
+ section: {row: {value: 0}},
+ });
+ const retiredSection = store.section;
+ if (!retiredSection) throw new Error('Expected initial section');
+ const retiredRow = retiredSection.row;
+ let rootNotifications = 0;
+ const stopRoot = subscribe(store, () => rootNotifications++, {sync: true});
+
+ if (remove === 'replace') store.section = {row: {value: 10}};
+ else delete store.section;
+
+ expect(rootNotifications).toBe(1);
+ expect(getInternal(retiredSection).parent).toBeNull();
+ expect(getInternal(retiredRow).parent).toBe(getInternal(retiredSection));
+ let retiredNotifications = 0;
+ const stopRetired = subscribe(
+ retiredSection,
+ () => retiredNotifications++,
+ {
+ sync: true,
+ },
+ );
+ retiredRow.value++;
+ expect(retiredNotifications).toBe(1);
+ expect(rootNotifications).toBe(1);
+ stopRetired();
+ stopRoot();
+ });
+
+ test('array truncation disconnects only removed index subtrees', () => {
+ const store = createClassyStore({
+ rows: Array.from({length: 4}, (_, index) => ({nested: {value: index}})),
+ });
+ const rows = store.rows;
+ const first = rows[0];
+ const removed = rows[3];
+ const removedNested = removed.nested;
+ let rootNotifications = 0;
+ const stopRoot = subscribe(store, () => rootNotifications++, {sync: true});
+
+ rows.length = 2;
+
+ expect(getInternal(rows).childInternals.size).toBe(1);
+ expect(getInternal(rows).childProxies.size).toBe(1);
+ expect(getInternal(first).parent).toBe(getInternal(rows));
+ expect(getInternal(removed).parent).toBeNull();
+ expect(getInternal(removedNested).parent).toBe(getInternal(removed));
+ removedNested.value++;
+ expect(rootNotifications).toBe(1);
+ first.nested.value++;
+ expect(rootNotifications).toBe(2);
+ stopRoot();
+ });
+
+ test('repeated growth and truncation releases every removed child entry', () => {
+ const store = createClassyStore({rows: [] as {value: number}[]});
+ const rows = store.rows;
+ for (let cycle = 0; cycle < 20; cycle++) {
+ rows.push(...Array.from({length: 32}, (_, index) => ({value: index})));
+ for (const row of rows) expect(row.value).toBeGreaterThanOrEqual(0);
+ expect(getInternal(rows).childInternals.size).toBe(32);
+ rows.length = 0;
+ expect(getInternal(rows).childInternals.size).toBe(0);
+ expect(getInternal(rows).childProxies.size).toBe(0);
+ }
+ });
+
+ test('truncation preserves object-valued properties that are not array indices', () => {
+ const rows = createClassyStore(
+ Object.assign([{value: 0}, {value: 1}], {
+ '01': {value: 2},
+ '1.0': {value: 3},
+ '4294967295': {value: 4},
+ }),
+ );
+ const index = rows[1];
+ const leadingZero = rows['01'];
+ const decimal = rows['1.0'];
+ const maximumLength = rows['4294967295'];
+
+ Reflect.set(rows, 'length', '1');
+
+ expect(rows.length).toBe(1);
+ expect(getInternal(index).parent).toBeNull();
+ expect(getInternal(leadingZero).parent).toBe(getInternal(rows));
+ expect(getInternal(decimal).parent).toBe(getInternal(rows));
+ expect(getInternal(maximumLength).parent).toBe(getInternal(rows));
+ expect(getInternal(rows).childInternals.size).toBe(3);
+ });
+
+ test('invalid array lengths leave existing child ownership intact', () => {
+ const rows = createClassyStore([{value: 0}, {value: 1}]);
+ const second = rows[1];
+ let notifications = 0;
+ const stop = subscribe(rows, () => notifications++, {sync: true});
+
+ expect(() => Reflect.set(rows, 'length', -1)).toThrow(RangeError);
+
+ expect(rows.length).toBe(2);
+ expect(rows[1]).toBe(second);
+ expect(getInternal(second).parent).toBe(getInternal(rows));
+ expect(notifications).toBe(0);
+ second.value++;
+ expect(notifications).toBe(1);
+ stop();
+ });
+
+ test.each([
+ {description: 'a rejected write', remove: 'replace'},
+ {description: 'a rejected deletion', remove: 'delete'},
+ ])('preserves the child cache after $description', ({remove}) => {
+ const target = {section: {value: 0}};
+ Object.defineProperty(target, 'section', {
+ writable: false,
+ configurable: true,
+ });
+ const store = createClassyStore(target);
+ const section = store.section;
+ if (remove === 'delete') {
+ Object.defineProperty(target, 'section', {
+ writable: true,
+ configurable: false,
+ });
+ }
+ let notifications = 0;
+ const stop = subscribe(store, () => notifications++, {sync: true});
+
+ if (remove === 'replace') Reflect.set(store, 'section', {value: 1});
+ else expect(Reflect.deleteProperty(store, 'section')).toBe(false);
+
+ expect(store.section).toBe(section);
+ expect(getInternal(store).childProxies.get('section')).toBe(section);
+ expect(getInternal(section).parent).toBe(getInternal(store));
+ expect(notifications).toBe(0);
+ section.value++;
+ expect(notifications).toBe(1);
+ stop();
+ });
+});
diff --git a/packages/classy-store/src/core/core.memory.test.ts b/packages/classy-store/src/core/core.memory.test.ts
new file mode 100644
index 0000000..775bad9
--- /dev/null
+++ b/packages/classy-store/src/core/core.memory.test.ts
@@ -0,0 +1,66 @@
+import {describe, expect, mock, test} from 'bun:test';
+import {snapshot} from '../snapshot/snapshot';
+import {createClassyStore, getInternal, getVersion, subscribe} from './core';
+
+describe('createClassyStore subtree lifecycle', () => {
+ test('retired descendants stop notifying and retaining their former root', () => {
+ const store = createClassyStore({section: {row: {value: 1}}});
+ const section = store.section;
+ const row = section.row;
+ store.section = {row: {value: 2}};
+ const rootListener = mock(() => {});
+ const retiredListener = mock(() => {});
+ const unsubscribeRoot = subscribe(store, rootListener, {sync: true});
+ const unsubscribeRetired = subscribe(section, retiredListener, {
+ sync: true,
+ });
+ const rootVersion = getVersion(store);
+
+ row.value = 3;
+
+ expect(rootListener).not.toHaveBeenCalled();
+ expect(retiredListener).toHaveBeenCalledTimes(1);
+ expect(getVersion(store)).toBe(rootVersion);
+ expect(getInternal(section).parent).toBeNull();
+ expect(getInternal(row).parent).toBe(getInternal(section));
+ expect(snapshot(section).row.value).toBe(3);
+ expect(store.section.row.value).toBe(2);
+ unsubscribeRoot();
+ unsubscribeRetired();
+ });
+
+ test('truncated slots create fresh proxies when the array grows again', () => {
+ const store = createClassyStore({rows: [{value: 1}, {value: 2}]});
+ const firstRow = store.rows[0];
+ const retiredRow = store.rows[1];
+
+ store.rows.length = 1;
+ store.rows.push({value: 3});
+
+ expect(store.rows[0]).toBe(firstRow);
+ expect(store.rows[1]).not.toBe(retiredRow);
+ expect(store.rows[1].value).toBe(3);
+ expect(snapshot(store).rows).toEqual([{value: 1}, {value: 3}]);
+ expect(getInternal(retiredRow).parent).toBeNull();
+ });
+
+ test('partial array truncation prunes removed children and reports the actual mutation', () => {
+ const rawRows = [{value: 0}, {value: 1}, {value: 2}, {value: 3}];
+ Object.defineProperty(rawRows, '1', {configurable: false});
+ const store = createClassyStore({rows: rawRows});
+ const retainedRow = store.rows[1];
+ const retiredRow = store.rows[3];
+ const listener = mock(() => {});
+ const unsubscribe = subscribe(store, listener, {sync: true});
+
+ expect(Reflect.set(store.rows, 'length', 0)).toBe(false);
+
+ expect(store.rows.length).toBe(2);
+ expect(store.rows[1]).toBe(retainedRow);
+ expect(getInternal(retiredRow).parent).toBeNull();
+ expect(getInternal(store.rows).childProxies.has('3')).toBe(false);
+ expect(listener).toHaveBeenCalledTimes(1);
+ expect(snapshot(store).rows).toEqual([{value: 0}, {value: 1}]);
+ unsubscribe();
+ });
+});
diff --git a/packages/classy-store/src/core/core.ts b/packages/classy-store/src/core/core.ts
index c570440..64ddab8 100644
--- a/packages/classy-store/src/core/core.ts
+++ b/packages/classy-store/src/core/core.ts
@@ -215,6 +215,34 @@ function getRoot(internal: StoreInternal): StoreInternal {
return current;
}
+/** Release a removed subtree's link to its former owner. */
+function detachChild(internal: StoreInternal, prop: string | symbol): void {
+ const child = internal.childInternals.get(prop);
+ if (!child) return;
+ child.parent = null;
+ internal.childProxies.delete(prop);
+ internal.childInternals.delete(prop);
+}
+
+/** Native array length writes remove indices without invoking DELETE traps. */
+function detachTruncatedChildren(
+ internal: StoreInternal,
+ length: number,
+): void {
+ for (const prop of internal.childInternals.keys()) {
+ if (typeof prop !== 'string') continue;
+ const index = Number(prop);
+ if (
+ Number.isInteger(index) &&
+ index >= length &&
+ index < 2 ** 32 - 1 &&
+ String(index) === prop
+ ) {
+ detachChild(internal, prop);
+ }
+ }
+}
+
// ββ Create proxy (recursive) ββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
@@ -256,18 +284,28 @@ function createStoreProxy(
const oldValue = Reflect.get(_target, prop);
if (Object.is(oldValue, value)) return true; // noop β same value
- // If the new value replaces a child proxy, clean it up.
- if (internal.childProxies.has(prop)) {
- internal.childProxies.delete(prop);
- internal.childInternals.delete(prop);
+ const oldLength =
+ Array.isArray(_target) && prop === 'length'
+ ? _target.length
+ : undefined;
+ const written = Reflect.set(_target, prop, value);
+ const truncated =
+ oldLength !== undefined && (_target as unknown[]).length < oldLength;
+ // A non-configurable array index can reject the requested length after
+ // higher indices were already removed. Clean up the actual truncation.
+ if (truncated) {
+ detachTruncatedChildren(internal, (_target as unknown[]).length);
}
+ if (!written && !truncated) return false;
+
+ // Keep the old child connected when a property rejects the write.
+ if (written) detachChild(internal, prop);
// Drop any cached bound method β the previous binding is dead now.
boundMethods.delete(prop);
- Reflect.set(_target, prop, value);
notifyMutation(internal);
- return true;
+ return written;
},
get(_target, prop, receiver) {
@@ -322,13 +360,10 @@ function createStoreProxy(
},
deleteProperty(_target, prop) {
- if (internal.childProxies.has(prop)) {
- internal.childProxies.delete(prop);
- internal.childInternals.delete(prop);
- }
- boundMethods.delete(prop);
const deleted = Reflect.deleteProperty(_target, prop);
if (deleted) {
+ detachChild(internal, prop);
+ boundMethods.delete(prop);
notifyMutation(internal);
}
return deleted;
diff --git a/packages/classy-store/src/frameworks/react/react.behavior.test.tsx b/packages/classy-store/src/frameworks/react/react.behavior.test.tsx
index 4bc5698..dd063de 100644
--- a/packages/classy-store/src/frameworks/react/react.behavior.test.tsx
+++ b/packages/classy-store/src/frameworks/react/react.behavior.test.tsx
@@ -1,5 +1,5 @@
import {afterEach, describe, expect, it, mock} from 'bun:test';
-import {act, type ReactNode} from 'react';
+import {act, type ReactNode, useLayoutEffect} from 'react';
import {createRoot, type Root} from 'react-dom/client';
import {createClassyStore} from '../../core/core';
import {shallowEqual} from '../../utils/equality/equality';
@@ -10,7 +10,16 @@ import {useClassyStore} from './react';
/** Flush queueMicrotask batching. */
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
+function createGate() {
+ let resolve: () => void = () => {};
+ const promise = new Promise((resolvePromise) => {
+ resolve = resolvePromise;
+ });
+ return {promise, resolve};
+}
+
let container: HTMLDivElement;
+const roots = new Set();
function setup(): void {
container = document.createElement('div');
@@ -18,12 +27,17 @@ function setup(): void {
}
function teardown(): void {
- document.body.removeChild(container);
+ act(() => {
+ for (const root of roots) root.unmount();
+ });
+ roots.clear();
+ container.remove();
}
/** Render a component and return the Root (for unmount tests). */
function render(element: ReactNode): Root {
const root = createRoot(container);
+ roots.add(root);
act(() => {
root.render(element);
});
@@ -49,17 +63,17 @@ class TestStore {
this.name = name;
}
- async asyncIncrement() {
+ async asyncIncrement(pause: Promise) {
this.count++;
- await Promise.resolve();
+ await pause;
this.count++;
}
- async asyncMultiAwait() {
+ async asyncMultiAwait(firstPause: Promise, secondPause: Promise) {
this.count++;
- await Promise.resolve();
+ await firstPause;
this.count++;
- await Promise.resolve();
+ await secondPause;
this.count++;
}
@@ -236,18 +250,22 @@ describe('Batching β rapid mutations in a loop', () => {
});
});
-// ββ Block 3: Set-then-revert β no re-render βββββββββββββββββββββββββββββββββ
+// ββ Block 3: Set-then-revert β committed output βββββββββββββββββββββββββββββββββ
-describe('Set-then-revert β no re-render when value returns to original', () => {
+describe('Set-then-revert β commits the restored value', () => {
afterEach(teardown);
it('primitive revert (selector mode)', async () => {
const s = createClassyStore(new TestStore());
const renderCount = mock(() => {});
+ const committedValues: number[] = [];
function Display() {
const count = useClassyStore(s, (state) => state.count);
renderCount();
+ useLayoutEffect(() => {
+ committedValues.push(count);
+ });
return {count}
;
}
@@ -262,19 +280,21 @@ describe('Set-then-revert β no re-render when value returns to original', () =
});
expect(container.textContent).toBe('0');
- expect(renderCount).toHaveBeenCalledTimes(1);
+ expect(committedValues.length).toBeGreaterThan(0);
+ expect(committedValues.every((value) => value === 0)).toBe(true);
});
- it('primitive revert (auto-tracked mode) β re-renders because snapshot ref changes', async () => {
- // Unlike selector mode (which compares the extracted value), auto-tracked mode
- // detects that the snapshot *object* is a new reference (version was bumped),
- // so `getAutoTrackSnapshot` builds a new tracking proxy β re-render.
+ it('primitive revert (auto-tracked mode)', async () => {
const s = createClassyStore(new TestStore());
const renderCount = mock(() => {});
+ const committedValues: number[] = [];
function Display() {
const snap = useClassyStore(s);
renderCount();
+ useLayoutEffect(() => {
+ committedValues.push(snap.count);
+ });
return {snap.count}
;
}
@@ -289,16 +309,21 @@ describe('Set-then-revert β no re-render when value returns to original', () =
});
expect(container.textContent).toBe('0');
- expect(renderCount).toHaveBeenCalledTimes(2);
+ expect(committedValues.length).toBeGreaterThan(0);
+ expect(committedValues.every((value) => value === 0)).toBe(true);
});
it('nested object prop revert (selector on user.name)', async () => {
const s = createClassyStore(new TestStore());
const renderCount = mock(() => {});
+ const committedValues: string[] = [];
function Display() {
const name = useClassyStore(s, (state) => state.user.name);
renderCount();
+ useLayoutEffect(() => {
+ committedValues.push(name);
+ });
return {name}
;
}
@@ -314,7 +339,8 @@ describe('Set-then-revert β no re-render when value returns to original', () =
});
expect(container.textContent).toBe('Alice');
- expect(renderCount).toHaveBeenCalledTimes(1);
+ expect(committedValues.length).toBeGreaterThan(0);
+ expect(committedValues.every((value) => value === 'Alice')).toBe(true);
});
it('unrelated prop revert β structural sharing preserves reference', async () => {
@@ -349,8 +375,6 @@ describe('Async methods β re-renders split across await boundaries', () => {
afterEach(teardown);
it('1 await, mutations before + after (selector)', async () => {
- // React batches both microtask notifications within act() into a single
- // commit, so we see 1 re-render (not 2) for mutations on both sides of an await.
const s = createClassyStore(new TestStore());
const renderCount = mock(() => {});
@@ -364,18 +388,21 @@ describe('Async methods β re-renders split across await boundaries', () => {
render( );
expect(renderCount).toHaveBeenCalledTimes(1);
- await act(async () => {
- await s.asyncIncrement();
- await flush();
+ const gate = createGate();
+ let completion: Promise | undefined;
+ act(() => {
+ completion = s.asyncIncrement(gate.promise);
});
+ expect(container.textContent).toBe('1');
+ await act(async () => {
+ gate.resolve();
+ await completion;
+ });
expect(container.textContent).toBe('2');
- expect(renderCount).toHaveBeenCalledTimes(2); // 1 initial + 1 re-render (batched)
});
it('2 awaits, mutations between each (selector)', async () => {
- // 3 mutations across 3 turns β React batches some of the microtask
- // notifications, resulting in 2 re-renders rather than 3.
const s = createClassyStore(new TestStore());
const renderCount = mock(() => {});
@@ -389,13 +416,22 @@ describe('Async methods β re-renders split across await boundaries', () => {
render( );
expect(renderCount).toHaveBeenCalledTimes(1);
- await act(async () => {
- await s.asyncMultiAwait();
- await flush();
+ const firstGate = createGate();
+ const secondGate = createGate();
+ let completion: Promise | undefined;
+ act(() => {
+ completion = s.asyncMultiAwait(firstGate.promise, secondGate.promise);
});
+ expect(container.textContent).toBe('1');
+
+ await act(async () => firstGate.resolve());
+ expect(container.textContent).toBe('2');
+ await act(async () => {
+ secondGate.resolve();
+ await completion;
+ });
expect(container.textContent).toBe('3');
- expect(renderCount).toHaveBeenCalledTimes(3); // 1 initial + 2 re-renders (partially batched)
});
it('mutations only before await (selector)', async () => {
@@ -458,13 +494,18 @@ describe('Async methods β re-renders split across await boundaries', () => {
render( );
expect(renderCount).toHaveBeenCalledTimes(1);
- await act(async () => {
- await s.asyncIncrement();
- await flush();
+ const gate = createGate();
+ let completion: Promise | undefined;
+ act(() => {
+ completion = s.asyncIncrement(gate.promise);
});
+ expect(container.textContent).toBe('1');
+ await act(async () => {
+ gate.resolve();
+ await completion;
+ });
expect(container.textContent).toBe('2');
- expect(renderCount).toHaveBeenCalledTimes(2); // 1 initial + 1 re-render (batched)
});
});
@@ -545,10 +586,7 @@ describe('Multiple components sharing the same store', () => {
expect(renderCountB).toHaveBeenCalledTimes(2);
});
- it('auto-tracked, both re-render when store notifies', async () => {
- // In auto-tracked mode, when the store notifies all subscribers, each
- // component's getSnapshot produces a new snapshot ref β both components
- // re-render, unlike selector mode which compares extracted values.
+ it('auto-tracked components update only for properties they read', async () => {
const s = createClassyStore(new TestStore());
const renderCountA = mock(() => {});
const renderCountB = mock(() => {});
@@ -581,7 +619,7 @@ describe('Multiple components sharing the same store', () => {
});
expect(renderCountA).toHaveBeenCalledTimes(2);
- expect(renderCountB).toHaveBeenCalledTimes(2);
+ expect(renderCountB).toHaveBeenCalledTimes(1);
});
it('batched multi-prop, both affected', async () => {
@@ -639,13 +677,13 @@ describe('Unmount safety', () => {
expect(container.textContent).toBe('0');
await act(async () => {
- s.increment(); // schedules microtask notification
- root.unmount(); // unsubscribes synchronously
- await flush(); // microtask fires with no listeners
+ s.increment();
+ root.unmount();
+ roots.delete(root);
+ await flush();
});
- // If we reach here without throwing, the test passes.
- expect(true).toBe(true);
+ expect(container.textContent).toBe('');
});
it('unmount and remount with same store shows latest state', async () => {
@@ -663,6 +701,7 @@ describe('Unmount safety', () => {
// Unmount
act(() => {
root.unmount();
+ roots.delete(root);
});
// Mutate while no component is mounted
diff --git a/packages/classy-store/src/frameworks/react/react.hydration.qa.test.tsx b/packages/classy-store/src/frameworks/react/react.hydration.qa.test.tsx
new file mode 100644
index 0000000..0a3b305
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/react.hydration.qa.test.tsx
@@ -0,0 +1,393 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {act, useEffect} from 'react';
+import {createRoot, hydrateRoot, type Root} from 'react-dom/client';
+import {renderToString} from 'react-dom/server';
+import {createClassyStore, getInternal} from '../../core/core';
+import type {Snapshot} from '../../types';
+import {prepareStoreForHydration, useClassyStore} from './react';
+import {DelayedHydrationView} from './testHelpers/delayedHydrationView/DelayedHydrationView';
+import type {HydrationStore} from './testHelpers/hydration.types';
+
+class HydrationQaStore {
+ loaded: {label: string} | null = null;
+
+ constructor(
+ public name: string,
+ public count = 1,
+ public details = {label: 'initial'},
+ public date = new Date('2020-01-01T00:00:00.000Z'),
+ ) {}
+
+ get summary() {
+ return `${this.name}:${this.count}:${this.details.label}`;
+ }
+}
+
+type Selection = {label: string};
+type Selector = (state: Snapshot) => Selection;
+
+const selectName: Selector = (state) => ({label: state.name});
+
+type HydrationQaViewProps = {
+ store: HydrationQaStore;
+ selector?: Selector;
+ isEqual?: (previous: Selection, next: Selection) => boolean;
+ onRead?: (value: Selection) => void;
+ onCommit?: (value: Selection) => void;
+};
+
+function HydrationQaView({
+ store,
+ selector = selectName,
+ isEqual,
+ onRead,
+ onCommit,
+}: HydrationQaViewProps) {
+ const value = useClassyStore(store, selector, {isEqual});
+ onRead?.(value);
+ useEffect(() => onCommit?.(value), [onCommit, value]);
+ return {value.label} ;
+}
+
+const mounted = new Map();
+
+function createContainer(html = '') {
+ const container = document.createElement('div');
+ container.innerHTML = html;
+ document.body.appendChild(container);
+ return container;
+}
+
+afterEach(async () => {
+ for (const [root, container] of mounted) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+ mounted.clear();
+});
+
+describe('prepareStoreForHydration independent QA', () => {
+ test('validates proxies, preserves identity, and does not subscribe', () => {
+ expect(() =>
+ prepareStoreForHydration(new HydrationQaStore('invalid')),
+ ).toThrow('object is not a store proxy');
+ const store = createClassyStore(new HydrationQaStore('initial'));
+ const internal = getInternal(store);
+
+ expect(prepareStoreForHydration(store)).toBe(store);
+ expect(internal.syncListeners.size).toBe(0);
+ expect(internal.batchedListeners.size).toBe(0);
+
+ store.name = 'live';
+ expect(prepareStoreForHydration(store)).toBe(store);
+ expect(renderToString( )).toBe(
+ 'initial ',
+ );
+ expect(store.name).toBe('live');
+ });
+
+ test('uses live values on a client-only mount even after preparation', async () => {
+ const store = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('initial')),
+ );
+ store.name = 'current';
+ const reads = mock((value: Selection) => value);
+ const container = createContainer();
+ const root = createRoot(container);
+ mounted.set(root, container);
+
+ await act(async () => {
+ root.render( );
+ });
+
+ expect(container.textContent).toBe('current');
+ expect(reads.mock.calls.every(([value]) => value.label === 'current')).toBe(
+ true,
+ );
+ act(() => {
+ store.name = 'edited';
+ });
+ expect(container.textContent).toBe('edited');
+ });
+
+ test('keeps nested data, getters, and replaced native leaves at their captured values for hydration', async () => {
+ const selector: Selector = (state) => ({
+ label: `${state.summary}:${state.date.getUTCFullYear()}`,
+ });
+ const serverStore = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('server')),
+ );
+ const clientStore = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('server')),
+ );
+ const container = createContainer(
+ renderToString(
+ ,
+ ),
+ );
+ const element = container.firstElementChild;
+ clientStore.name = 'live';
+ clientStore.count = 8;
+ clientStore.details.label = 'changed';
+ clientStore.date = new Date('2026-01-01T00:00:00.000Z');
+ const reads = mock((value: Selection) => value);
+ const onRecoverableError = mock(() => {});
+
+ await act(async () => {
+ const root = hydrateRoot(
+ container,
+ ,
+ {onRecoverableError},
+ );
+ mounted.set(root, container);
+ });
+
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.firstElementChild).toBe(element);
+ expect(reads.mock.calls[0][0].label).toBe('server:1:initial:2020');
+ expect(container.textContent).toBe('live:8:changed:2026');
+ expect(serverStore.summary).toBe('server:1:initial');
+ });
+
+ test('never evaluates a selector against initial data on a client-only mount', async () => {
+ const store = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('initial')),
+ );
+ store.loaded = {label: 'loaded'};
+ const selector: Selector = (state) => {
+ if (!state.loaded) throw new Error('Selector read unloaded initial data');
+ return {label: state.loaded.label};
+ };
+ const container = createContainer();
+ const root = createRoot(container);
+ mounted.set(root, container);
+
+ await act(async () => {
+ root.render( );
+ });
+ expect(container.textContent).toBe('loaded');
+ });
+
+ test('reselects current state when selectors, equality, and prepared stores change after hydration', async () => {
+ const firstStore = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('first')),
+ );
+ const secondStore = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('second', 2)),
+ );
+ const container = createContainer(
+ renderToString( ),
+ );
+ const onRecoverableError = mock(() => {});
+ let root!: Root;
+ await act(async () => {
+ root = hydrateRoot(container, , {
+ onRecoverableError,
+ });
+ mounted.set(root, container);
+ });
+
+ act(() => {
+ firstStore.name = 'first live';
+ secondStore.name = 'second live';
+ secondStore.count = 9;
+ });
+ expect(container.textContent).toBe('first live');
+ const equalLabels = (previous: Selection, next: Selection) =>
+ previous.label === next.label;
+ await act(async () => {
+ root.render(
+ ({label: `${state.name}:${state.count}`})}
+ isEqual={equalLabels}
+ />,
+ );
+ });
+
+ expect(container.textContent).toBe('second live:9');
+ expect(getInternal(firstStore).syncListeners.size).toBe(0);
+ expect(getInternal(secondStore).syncListeners.size).toBe(1);
+ await act(async () => {
+ root.render(
+ ({label: `${state.count}`})}
+ isEqual={Object.is}
+ />,
+ );
+ });
+ expect(container.textContent).toBe('9');
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ });
+
+ test('retains initial data for a second root hydrated after the first root updates', async () => {
+ const serverStore = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('initial')),
+ );
+ const clientStore = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('initial')),
+ );
+ const html = renderToString( );
+ const firstContainer = createContainer(html);
+ const secondContainer = createContainer(html);
+ const secondElement = secondContainer.firstElementChild;
+ const onRecoverableError = mock(() => {});
+ await act(async () => {
+ const root = hydrateRoot(
+ firstContainer,
+ ,
+ {onRecoverableError},
+ );
+ mounted.set(root, firstContainer);
+ });
+ act(() => {
+ clientStore.name = 'updated';
+ });
+ expect(firstContainer.textContent).toBe('updated');
+ expect(secondContainer.textContent).toBe('initial');
+
+ const reads = mock((value: Selection) => value);
+ await act(async () => {
+ const root = hydrateRoot(
+ secondContainer,
+ ,
+ {onRecoverableError},
+ );
+ mounted.set(root, secondContainer);
+ });
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(secondContainer.firstElementChild).toBe(secondElement);
+ expect(reads.mock.calls[0][0].label).toBe('initial');
+ expect(secondContainer.textContent).toBe('updated');
+ expect(getInternal(clientStore).syncListeners.size).toBe(2);
+ });
+
+ test('does not affect an unprepared request store', () => {
+ const prepared = prepareStoreForHydration(
+ createClassyStore(new HydrationQaStore('prepared initial')),
+ );
+ const unprepared = createClassyStore(new HydrationQaStore('request'));
+ prepared.name = 'prepared live';
+ unprepared.name = 'request live';
+
+ expect(renderToString( )).toBe(
+ 'prepared initial ',
+ );
+ expect(renderToString( )).toBe(
+ 'request live ',
+ );
+ });
+
+ test.each([
+ {
+ description: 'prepared unchanged state with custom equality',
+ prepare: true,
+ update: false,
+ equality: true,
+ },
+ {
+ description: 'prepared unchanged state with default equality',
+ prepare: true,
+ update: false,
+ equality: false,
+ },
+ {
+ description: 'prepared unselected changes',
+ prepare: true,
+ update: true,
+ equality: true,
+ },
+ {
+ description: 'unprepared unchanged state',
+ prepare: false,
+ update: false,
+ equality: true,
+ },
+ ])('retains equal selection identity across hydration for $description', async ({
+ prepare,
+ update,
+ equality,
+ }) => {
+ const serverStore = createClassyStore(new HydrationQaStore('initial'));
+ const clientStore = createClassyStore(new HydrationQaStore('initial'));
+ if (prepare) {
+ prepareStoreForHydration(serverStore);
+ prepareStoreForHydration(clientStore);
+ }
+ if (update) clientStore.count++;
+ const container = createContainer(
+ renderToString( ),
+ );
+ const onCommit = mock((_value: Selection) => {});
+ const isEqual = (previous: Selection, next: Selection) =>
+ previous.label === next.label;
+ const onRecoverableError = mock(() => {});
+ await act(async () => {
+ const root = hydrateRoot(
+ container,
+ ,
+ {onRecoverableError},
+ );
+ mounted.set(root, container);
+ });
+
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.textContent).toBe('initial');
+ expect(onCommit).toHaveBeenCalledTimes(1);
+ });
+
+ test('retains tracked snapshot identity when only unread state changes before hydration', async () => {
+ const serverStore = prepareStoreForHydration(
+ createClassyStore({user: {name: 'initial'}, count: 1}),
+ );
+ const clientStore = prepareStoreForHydration(
+ createClassyStore({user: {name: 'initial'}, count: 1}),
+ );
+ const onCommit = mock((_state: Snapshot) => {});
+ const gate = {read() {}, release() {}};
+ const onValueCommit = () => {};
+ function useTrackedName(store: HydrationStore) {
+ const state = useClassyStore(store);
+ useEffect(() => onCommit(state), [state]);
+ return state.user.name;
+ }
+ const container = createContainer(
+ renderToString(
+ ,
+ ),
+ );
+ clientStore.count++;
+ const onRecoverableError = mock(() => {});
+ await act(async () => {
+ const root = hydrateRoot(
+ container,
+ ,
+ {onRecoverableError},
+ );
+ mounted.set(root, container);
+ });
+
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.textContent).toBe('initial');
+ expect(onCommit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/react.hydration.test.tsx b/packages/classy-store/src/frameworks/react/react.hydration.test.tsx
new file mode 100644
index 0000000..d0b4017
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/react.hydration.test.tsx
@@ -0,0 +1,226 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {act, Suspense, useEffect} from 'react';
+import {hydrateRoot, type Root} from 'react-dom/client';
+import {renderToString} from 'react-dom/server';
+import {createClassyStore, getInternal} from '../../core/core';
+import {persist} from '../../utils/persist/persist';
+import {
+ createStoreHook,
+ prepareStoreForHydration,
+ useClassyStore,
+} from './react';
+import {DelayedHydrationView} from './testHelpers/delayedHydrationView/DelayedHydrationView';
+import type {
+ HydrationGate,
+ HydrationStore,
+ UseHydrationValue,
+} from './testHelpers/hydration.types';
+
+function createGate(initiallyReady = false): HydrationGate {
+ let ready = initiallyReady;
+ let resolve: () => void = () => {};
+ const promise = new Promise((resolvePromise) => {
+ resolve = resolvePromise;
+ });
+ return {
+ read() {
+ if (!ready) throw promise;
+ },
+ release() {
+ ready = true;
+ resolve();
+ },
+ };
+}
+
+function useSelectedValue(store: HydrationStore) {
+ return useClassyStore(store, (state) => `${state.user.name}:${state.count}`);
+}
+
+function useTrackedValue(store: HydrationStore) {
+ const state = useClassyStore(store);
+ return `${state.user.name}:${state.count}`;
+}
+
+const modes: {
+ description: string;
+ createReader: (store: HydrationStore) => UseHydrationValue;
+}[] = [
+ {description: 'direct selector', createReader: () => useSelectedValue},
+ {description: 'direct auto-tracking', createReader: () => useTrackedValue},
+ {
+ description: 'bound selector',
+ createReader: (store) => {
+ const useStore = createStoreHook(store);
+ return () => useStore((state) => `${state.user.name}:${state.count}`);
+ },
+ },
+ {
+ description: 'bound auto-tracking',
+ createReader: (store) => {
+ const useStore = createStoreHook(store);
+ return () => {
+ const state = useStore();
+ return `${state.user.name}:${state.count}`;
+ };
+ },
+ },
+];
+
+type HydrationShellProps = {
+ gate: HydrationGate;
+ store: HydrationStore;
+ useValue: UseHydrationValue;
+ onCommit: (value: string) => void;
+ onMount?: () => undefined | (() => void);
+};
+
+function HydrationShell({
+ gate,
+ store,
+ useValue,
+ onCommit,
+ onMount,
+}: HydrationShellProps) {
+ useEffect(() => onMount?.(), [onMount]);
+ return (
+
+ Request shell
+ Loading}>
+
+
+
+ );
+}
+
+const mounted = new Map();
+
+afterEach(async () => {
+ for (const [root, container] of mounted) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+ mounted.clear();
+});
+
+describe('React delayed Suspense hydration', () => {
+ test.each(
+ modes,
+ )('preserves server content when persistence updates before the first $description read', async ({
+ createReader,
+ }) => {
+ const serverStore = prepareStoreForHydration(
+ createClassyStore({user: {name: 'Server'}, count: 1}),
+ );
+ const container = document.createElement('div');
+ container.innerHTML = renderToString(
+ {}}
+ />,
+ );
+ document.body.appendChild(container);
+ const serverElement = container.querySelector('[data-hydration-value]');
+ expect(serverElement?.textContent).toBe('Server:1');
+
+ const clientStore = prepareStoreForHydration(
+ createClassyStore({user: {name: 'Server'}, count: 1}),
+ );
+ const gate = createGate();
+ const onCommit = mock((value: string) => value);
+ const onRecoverableError = mock(() => {});
+ const storage = {
+ getItem: mock(async () =>
+ JSON.stringify({version: 0, state: {user: {name: 'Saved'}, count: 8}}),
+ ),
+ setItem: mock(() => {}),
+ removeItem: mock(() => {}),
+ };
+ const persistence = persist(clientStore, {
+ name: 'delayed-request',
+ storage,
+ skipHydration: true,
+ });
+ let rehydration: Promise | undefined;
+ const onMount = () => {
+ rehydration = persistence.rehydrate();
+ return () => persistence.unsubscribe();
+ };
+
+ let root!: Root;
+ await act(async () => {
+ root = hydrateRoot(
+ container,
+ ,
+ {onRecoverableError},
+ );
+ mounted.set(root, container);
+ });
+ await act(async () => {
+ await rehydration;
+ });
+
+ // The shell has committed and persisted state has arrived, but the child
+ // has never read its store and the boundary still shows untouched SSR DOM.
+ expect(storage.getItem).toHaveBeenCalledTimes(1);
+ expect(clientStore.user.name).toBe('Saved');
+ expect(clientStore.count).toBe(8);
+ // Repeated setup must not replace the snapshot already used by the server.
+ expect(prepareStoreForHydration(clientStore)).toBe(clientStore);
+ expect(onCommit).not.toHaveBeenCalled();
+ expect(container.querySelector('[data-hydration-value]')).toBe(
+ serverElement,
+ );
+ expect(serverElement?.textContent).toBe('Server:1');
+
+ await act(async () => {
+ gate.release();
+ });
+
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.querySelector('[data-hydration-value]')).toBe(
+ serverElement,
+ );
+ expect(serverElement?.textContent).toBe('Saved:8');
+ expect(onCommit.mock.calls.map(([value]) => value)).toEqual([
+ 'Server:1',
+ 'Saved:8',
+ ]);
+ expect(getInternal(clientStore).syncListeners.size).toBe(1);
+ expect(getInternal(clientStore).batchedListeners.size).toBe(1);
+ expect(storage.setItem).not.toHaveBeenCalled();
+
+ await act(async () => {
+ clientStore.user.name = 'Edited';
+ clientStore.count = 9;
+ });
+ expect(serverElement?.textContent).toBe('Edited:9');
+ expect(serverStore.user.name).toBe('Server');
+ expect(serverStore.count).toBe(1);
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+
+ const commitsBeforeUnmount = onCommit.mock.calls.length;
+ await act(async () => root.unmount());
+ mounted.delete(root);
+ container.remove();
+ expect(getInternal(clientStore).syncListeners.size).toBe(0);
+ expect(getInternal(clientStore).batchedListeners.size).toBe(0);
+ clientStore.count++;
+ await Promise.resolve();
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+ expect(onCommit).toHaveBeenCalledTimes(commitsBeforeUnmount);
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/react.lifecycle.test.tsx b/packages/classy-store/src/frameworks/react/react.lifecycle.test.tsx
new file mode 100644
index 0000000..2ee3a99
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/react.lifecycle.test.tsx
@@ -0,0 +1,426 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {
+ act,
+ type ReactNode,
+ StrictMode,
+ Suspense,
+ startTransition,
+ useEffect,
+} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {createClassyStore, getInternal} from '../../core/core';
+import {PROXYABLE} from '../../utils/internal/internal';
+import {useClassyStore, useLocalStore} from './react';
+
+class LifecycleStore {
+ first = 'First';
+ second = 'Second';
+}
+
+type Field = 'first' | 'second';
+
+function useSelectedField(store: LifecycleStore, field: Field) {
+ return useClassyStore(store, (state) => state[field]);
+}
+
+function useTrackedField(store: LifecycleStore, field: Field) {
+ return useClassyStore(store)[field];
+}
+
+const modes = [
+ {description: 'selector', useField: useSelectedField},
+ {description: 'auto-tracked', useField: useTrackedField},
+];
+
+class NestedLifecycleStore extends LifecycleStore {
+ static [PROXYABLE] = true;
+}
+
+const trackingShapes = [
+ {
+ description: 'plain object',
+ createValue: () => ({first: 'First', second: 'Second'}),
+ },
+ {
+ description: 'null-prototype object',
+ createValue: () =>
+ Object.assign(Object.create(null), {
+ first: 'First',
+ second: 'Second',
+ }) as LifecycleStore,
+ },
+ {
+ description: 'nested opted-in class',
+ createValue: () => new NestedLifecycleStore(),
+ },
+];
+
+const mountedRoots = new Map();
+
+function mount(element: ReactNode) {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mountedRoots.set(root, container);
+ act(() => root.render(element));
+ return {root, container};
+}
+
+function unmount(root: Root) {
+ const container = mountedRoots.get(root);
+ act(() => root.unmount());
+ container?.remove();
+ mountedRoots.delete(root);
+}
+
+function subscriberCount(store: LifecycleStore) {
+ const internal = getInternal(store);
+ return internal.batchedListeners.size + internal.syncListeners.size;
+}
+
+function createSuspension() {
+ let ready = false;
+ let resolve: () => void = () => {};
+ const promise = new Promise((resolvePromise) => {
+ resolve = resolvePromise;
+ });
+ return {
+ read() {
+ if (!ready) throw promise;
+ },
+ resolve() {
+ ready = true;
+ resolve();
+ },
+ };
+}
+
+afterEach(() => {
+ for (const root of mountedRoots.keys()) unmount(root);
+});
+
+describe('useClassyStore lifecycle', () => {
+ test.each(
+ trackingShapes,
+ )('tracks nested conditional reads on a $description', async ({
+ createValue,
+ }) => {
+ const store = createClassyStore({node: createValue()});
+ const renderCount = mock(() => {});
+
+ function Display({field}: {field: Field}) {
+ const value = useClassyStore(store).node[field];
+ renderCount();
+ return {value} ;
+ }
+
+ const {root, container} = mount( );
+ renderCount.mockClear();
+ await act(async () => {
+ store.node.second = 'Hidden update';
+ });
+ expect(renderCount).not.toHaveBeenCalled();
+ act(() => root.render( ));
+ expect(container.textContent).toBe('Hidden update');
+ renderCount.mockClear();
+ await act(async () => {
+ store.node.first = 'Old selection';
+ });
+ expect(renderCount).not.toHaveBeenCalled();
+ await act(async () => {
+ store.node.second = 'Visible update';
+ });
+ expect(container.textContent).toBe('Visible update');
+ });
+
+ test.each(
+ modes,
+ )('cleans up StrictMode subscriptions and supports remounting ($description)', async ({
+ useField,
+ }) => {
+ const store = createClassyStore(new LifecycleStore());
+
+ function Display() {
+ const value = useField(store, 'first');
+ return {value} ;
+ }
+
+ const {root, container} = mount(
+
+
+ ,
+ );
+ expect(subscriberCount(store)).toBe(1);
+ await act(async () => {
+ store.first = 'Updated';
+ });
+ expect(container.textContent).toBe('Updated');
+
+ unmount(root);
+ expect(subscriberCount(store)).toBe(0);
+ store.first = 'While unmounted';
+
+ const remount = mount(
+
+
+ ,
+ );
+ expect(remount.container.textContent).toBe('While unmounted');
+ expect(subscriberCount(store)).toBe(1);
+ unmount(remount.root);
+ expect(subscriberCount(store)).toBe(0);
+ });
+
+ test.each(
+ modes,
+ )('switches store instances even when selected values are equal ($description)', async ({
+ useField,
+ }) => {
+ const firstStore = createClassyStore(new LifecycleStore());
+ const secondStore = createClassyStore(new LifecycleStore());
+ const renderCount = mock(() => {});
+
+ function Display({store}: {store: LifecycleStore}) {
+ const value = useField(store, 'first');
+ renderCount();
+ return {value} ;
+ }
+
+ const {root, container} = mount( );
+ act(() => root.render( ));
+ expect(container.textContent).toBe('First');
+ expect(subscriberCount(firstStore)).toBe(0);
+ expect(subscriberCount(secondStore)).toBe(1);
+ renderCount.mockClear();
+
+ await act(async () => {
+ firstStore.first = 'Old store';
+ });
+ expect(renderCount).not.toHaveBeenCalled();
+ expect(container.textContent).toBe('First');
+
+ await act(async () => {
+ secondStore.first = 'New store';
+ });
+ expect(container.textContent).toBe('New store');
+ });
+
+ test.each(
+ modes,
+ )('tracks the currently rendered conditional property ($description)', async ({
+ useField,
+ }) => {
+ const store = createClassyStore(new LifecycleStore());
+ const renderCount = mock(() => {});
+
+ function Display({field}: {field: Field}) {
+ const value = useField(store, field);
+ renderCount();
+ return {value} ;
+ }
+
+ const {root, container} = mount( );
+ renderCount.mockClear();
+ await act(async () => {
+ store.second = 'Changed while hidden';
+ });
+ expect(renderCount).not.toHaveBeenCalled();
+
+ act(() => root.render( ));
+ expect(container.textContent).toBe('Changed while hidden');
+ renderCount.mockClear();
+ await act(async () => {
+ store.first = 'No longer read';
+ });
+ expect(renderCount).not.toHaveBeenCalled();
+
+ await act(async () => {
+ store.second = 'Current property';
+ });
+ expect(container.textContent).toBe('Current property');
+ });
+
+ test.each(
+ modes,
+ )('keeps the committed selection live while a transition suspends ($description)', async ({
+ useField,
+ }) => {
+ const store = createClassyStore(new LifecycleStore());
+ const suspension = createSuspension();
+ const attemptedFields = mock((_field: Field) => {});
+
+ function Display({field}: {field: Field}) {
+ const value = useField(store, field);
+ attemptedFields(field);
+ if (field === 'second') suspension.read();
+ return {value} ;
+ }
+
+ const {root, container} = mount(
+
+
+ ,
+ );
+ await act(async () => {
+ startTransition(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ });
+ expect(attemptedFields).toHaveBeenCalledWith('second');
+ expect(container.textContent).toBe('First');
+
+ await act(async () => {
+ store.first = 'Committed update';
+ });
+ expect(container.textContent).toBe('Committed update');
+ await act(async () => {
+ store.second = 'Pending update';
+ });
+ expect(container.textContent).toBe('Committed update');
+
+ await act(async () => suspension.resolve());
+ expect(container.textContent).toBe('Pending update');
+ expect(subscriberCount(store)).toBe(1);
+ });
+
+ test.each(
+ modes,
+ )('discards reads from an abandoned suspended render ($description)', async ({
+ useField,
+ }) => {
+ const store = createClassyStore(new LifecycleStore());
+ const suspension = createSuspension();
+ const renderCount = mock(() => {});
+
+ function Display({field}: {field: Field}) {
+ const value = useField(store, field);
+ renderCount();
+ if (field === 'second') suspension.read();
+ return {value} ;
+ }
+
+ const {root, container} = mount(
+
+
+ ,
+ );
+ await act(async () => {
+ startTransition(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ });
+ act(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ renderCount.mockClear();
+ await act(async () => {
+ store.second = 'Abandoned branch';
+ });
+ expect(renderCount).not.toHaveBeenCalled();
+ expect(container.textContent).toBe('First');
+
+ await act(async () => suspension.resolve());
+ await act(async () => {
+ store.first = 'Still subscribed';
+ });
+ expect(container.textContent).toBe('Still subscribed');
+ });
+
+ test.each(
+ modes,
+ )('reads the latest state when an initial suspended mount resumes ($description)', async ({
+ useField,
+ }) => {
+ const store = createClassyStore(new LifecycleStore());
+ const suspension = createSuspension();
+
+ function Display() {
+ const value = useField(store, 'first');
+ suspension.read();
+ return {value} ;
+ }
+
+ const {container} = mount(
+
+
+ ,
+ );
+ expect(container.textContent).toBe('Loading');
+ expect(subscriberCount(store)).toBe(0);
+ store.first = 'Before commit';
+ await act(async () => suspension.resolve());
+ expect(container.textContent).toBe('Before commit');
+ expect(subscriberCount(store)).toBe(1);
+ });
+});
+
+describe('useLocalStore lifecycle', () => {
+ test.each(
+ modes,
+ )('isolates mounted instances and replaces state on a keyed remount ($description)', async ({
+ useField,
+ }) => {
+ const stores = new Map();
+
+ function Display({id, label}: {id: string; label: string}) {
+ const store = useLocalStore(() => new LifecycleStore());
+ const value = useField(store, 'first');
+ useEffect(() => {
+ stores.set(id, store);
+ return () => {
+ stores.delete(id);
+ };
+ }, [id, store]);
+ return (
+
+ {label}:{value}|
+
+ );
+ }
+
+ const {root, container} = mount(
+
+
+
+ ,
+ );
+ const firstStore = stores.get('first');
+ const secondStore = stores.get('second');
+ expect(firstStore).toBeDefined();
+ expect(secondStore).toBeDefined();
+ expect(firstStore).not.toBe(secondStore);
+ if (!firstStore || !secondStore) throw new Error('Stores did not mount');
+
+ await act(async () => {
+ firstStore.first = 'Local update';
+ });
+ expect(container.textContent).toBe('A:Local update|B:First|');
+ act(() => {
+ root.render(
+
+
+
+ ,
+ );
+ });
+ expect(stores.get('first')).toBe(firstStore);
+ expect(stores.get('second')).not.toBe(secondStore);
+ expect(container.textContent).toBe('Updated A:Local update|New B:First|');
+ expect(subscriberCount(secondStore)).toBe(0);
+ unmount(root);
+ expect(stores.size).toBe(0);
+ expect(subscriberCount(firstStore)).toBe(0);
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/react.performance.qa.test.tsx b/packages/classy-store/src/frameworks/react/react.performance.qa.test.tsx
new file mode 100644
index 0000000..0de328f
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/react.performance.qa.test.tsx
@@ -0,0 +1,221 @@
+import {afterEach, describe, expect, test} from 'bun:test';
+import {act, createElement, type ReactNode} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {createClassyStore, getInternal} from '../../core/core';
+import {snapshot} from '../../snapshot/snapshot';
+import type {Snapshot} from '../../types';
+import type {
+ PerformanceQaCommit,
+ PerformanceQaStore,
+} from './testHelpers/performanceQa.types';
+import {PerformanceQaComputedView} from './testHelpers/performanceQaComputedView/PerformanceQaComputedView';
+import {PerformanceQaView} from './testHelpers/performanceQaView/PerformanceQaView';
+
+const mounted = new Map();
+
+function mount(element: ReactNode) {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mounted.set(root, container);
+ act(() => root.render(element));
+ return {root, container};
+}
+
+function createStore(size = 2) {
+ return createClassyStore({
+ rows: Array.from({length: size}, (_, index) => ({
+ name: `Row ${index}`,
+ ignored: 0,
+ })),
+ ignored: 0,
+ });
+}
+
+function createCommits() {
+ const counts = new Map();
+ const states = new Map>();
+ const onCommit: PerformanceQaCommit = (id, state) => {
+ counts.set(id, (counts.get(id) ?? 0) + 1);
+ states.set(id, state);
+ };
+ return {counts, states, onCommit};
+}
+
+afterEach(() => {
+ for (const [root, container] of mounted) {
+ act(() => root.unmount());
+ container.remove();
+ }
+ mounted.clear();
+});
+
+describe('useClassyStore performance independent QA', () => {
+ test('keeps shared snapshot targets independent across many consumer read sets', () => {
+ const store = createStore(128);
+ const {counts, states, onCommit} = createCommits();
+ const {container} = mount(
+ Array.from({length: 128}, (_, index) =>
+ createElement(PerformanceQaView, {
+ key: index,
+ id: String(index),
+ store,
+ index,
+ onCommit,
+ }),
+ ),
+ );
+ expect([...counts.values()]).toEqual(Array(128).fill(1));
+ expect(states.get('0')).not.toBe(states.get('1'));
+
+ act(() => {
+ store.rows[37].name = 'Changed';
+ });
+ expect([...counts.entries()].filter(([, count]) => count > 1)).toEqual([
+ ['37', 2],
+ ]);
+ expect(container.querySelector('[data-consumer="37"]')?.textContent).toBe(
+ ':Changed',
+ );
+
+ act(() => {
+ store.ignored += 1;
+ store.rows[37].ignored += 1;
+ store.rows[90].ignored += 1;
+ });
+ expect([...counts.entries()].filter(([, count]) => count > 1)).toEqual([
+ ['37', 2],
+ ]);
+
+ act(() => {
+ store.rows[90].name = 'Other';
+ });
+ expect([...counts.entries()].filter(([, count]) => count > 1)).toEqual([
+ ['37', 2],
+ ['90', 2],
+ ]);
+ });
+
+ test.each([
+ {
+ description: 'root',
+ select: (state: Snapshot) => state,
+ },
+ {
+ description: 'array',
+ select: (state: Snapshot) => state.rows,
+ },
+ {
+ description: 'nested row',
+ select: (state: Snapshot) => state.rows[0],
+ },
+ ])('protects shared $description targets starting with the first consumer', ({
+ select,
+ }) => {
+ const store = createStore();
+ const {states, counts, onCommit} = createCommits();
+ const {container} = mount([
+ ,
+ ,
+ ]);
+
+ for (const id of ['first', 'second']) {
+ const state = states.get(id);
+ expect(state).toBeDefined();
+ if (!state) throw new Error('Missing committed snapshot');
+ const target = select(state);
+ const prototype = Object.getPrototypeOf(target);
+ expect(
+ Reflect.defineProperty(target, 'injected', {value: 'poison'}),
+ ).toBe(false);
+ expect(Reflect.setPrototypeOf(target, {injected: 'poison'})).toBe(false);
+ expect(Reflect.preventExtensions(target)).toBe(false);
+ expect(Reflect.set(target, 'injected', 'poison')).toBe(false);
+ expect(
+ Reflect.deleteProperty(target, Array.isArray(target) ? '0' : 'ignored'),
+ ).toBe(false);
+ expect(Object.getPrototypeOf(target)).toBe(prototype);
+ expect('injected' in target).toBe(false);
+ }
+
+ expect(snapshot(store).rows.map((row) => row.name)).toEqual([
+ 'Row 0',
+ 'Row 1',
+ ]);
+ expect(container.textContent).toBe(':Row 0:Row 1');
+ act(() => {
+ store.rows[0].name = 'Still live';
+ });
+ expect(container.textContent).toBe(':Still live:Row 1');
+ expect(counts.get('first')).toBe(2);
+ expect(counts.get('second')).toBe(1);
+ });
+
+ test('keeps target reuse independent when a consumer changes stores and unmounts', () => {
+ const firstStore = createStore();
+ const secondStore = createStore();
+ const {counts, onCommit} = createCommits();
+ const view = (store: PerformanceQaStore) => (
+
+ );
+ const {root, container} = mount(view(firstStore));
+ act(() => root.render(view(secondStore)));
+ expect(getInternal(firstStore).syncListeners.size).toBe(0);
+ expect(getInternal(secondStore).syncListeners.size).toBe(1);
+ const before = counts.get('current');
+ act(() => {
+ firstStore.rows[0].name = 'Retired';
+ });
+ expect(counts.get('current')).toBe(before);
+ expect(container.textContent).toBe(':Row 0');
+ act(() => {
+ secondStore.rows[0].name = 'Current';
+ });
+ expect(container.textContent).toBe(':Current');
+ act(() => root.render(null));
+ expect(getInternal(secondStore).syncListeners.size).toBe(0);
+ });
+
+ test('rechecks mutable getter results if they become frozen after first access', () => {
+ class DerivedStore {
+ name = 'Initial';
+
+ get derived() {
+ return {child: {name: this.name}};
+ }
+ }
+ const store = createClassyStore(new DerivedStore());
+ const derived = snapshot(store).derived;
+ expect(Object.isFrozen(derived)).toBe(false);
+ const {root, container} = mount(
+ ,
+ );
+ expect(container.textContent).toBe('first:Initial');
+ Object.freeze(derived);
+ act(() =>
+ root.render( ),
+ );
+ expect(container.textContent).toBe('again:Initial');
+ act(() => {
+ store.name = 'Updated';
+ });
+ expect(container.textContent).toBe('again:Updated');
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/react.qa.test.tsx b/packages/classy-store/src/frameworks/react/react.qa.test.tsx
new file mode 100644
index 0000000..fefc029
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/react.qa.test.tsx
@@ -0,0 +1,213 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {
+ act,
+ memo,
+ type ReactNode,
+ Suspense,
+ startTransition,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {createClassyStore} from '../../core/core';
+import {useClassyStore} from './react';
+
+const mountedRoots = new Map();
+
+function mount(element: ReactNode) {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mountedRoots.set(root, container);
+ act(() => root.render(element));
+ return {root, container};
+}
+
+afterEach(() => {
+ for (const [root, container] of mountedRoots) {
+ act(() => root.unmount());
+ container.remove();
+ }
+ mountedRoots.clear();
+});
+
+describe('useClassyStore independent QA', () => {
+ test('preserves nested snapshot identity across local state renders', () => {
+ const store = createClassyStore({items: ['First']});
+ const effect = mock(() => {});
+
+ function Display() {
+ const state = useClassyStore(store);
+ const items = state.items;
+ const updates = useRef(0);
+ const [processed, setProcessed] = useState([]);
+ useEffect(() => {
+ effect();
+ // Bound the probe so an unstable dependency cannot hang the suite.
+ if (updates.current < 3) {
+ updates.current++;
+ setProcessed([...items]);
+ }
+ }, [items]);
+ return (
+
+ {processed[0]}:{updates.current}
+
+ );
+ }
+
+ const {container} = mount( );
+ expect(container.textContent).toBe('First:1');
+ expect(effect).toHaveBeenCalledTimes(1);
+ });
+
+ test('recovers from a selector that throws between related property writes', async () => {
+ const store = createClassyStore({
+ selected: 'first',
+ records: {first: {name: 'First'}} as Record,
+ });
+ const invalidSelections = mock(() => {});
+ const selectName = (state: typeof store) => {
+ const record = state.records[state.selected];
+ if (!record) invalidSelections();
+ return record.name;
+ };
+
+ function Display() {
+ const value = useClassyStore(store, selectName);
+ return {value} ;
+ }
+
+ const {container} = mount( );
+ await act(async () => {
+ store.selected = 'second';
+ store.records = {second: {name: 'Second'}};
+ });
+ expect(invalidSelections).toHaveBeenCalled();
+ expect(container.textContent).toBe('Second');
+ await act(async () => {
+ store.records.second.name = 'Updated';
+ });
+ expect(container.textContent).toBe('Updated');
+ });
+
+ test('recovers while array deletion produces transient empty slots', async () => {
+ const store = createClassyStore({
+ items: [{name: 'First'}, {name: 'Second'}],
+ });
+
+ function Display() {
+ const names = useClassyStore(store, (state) =>
+ state.items.map((item) => item.name).join(','),
+ );
+ return {names} ;
+ }
+
+ const {container} = mount( );
+ await act(async () => {
+ store.items.splice(0, 1);
+ });
+ expect(container.textContent).toBe('Second');
+ await act(async () => {
+ store.items[0].name = 'Updated';
+ });
+ expect(container.textContent).toBe('Updated');
+ });
+
+ test('keeps committed nested reads live through an abandoned transition', async () => {
+ const store = createClassyStore({node: {first: 'First', second: 'Second'}});
+ const pending = new Promise(() => {});
+ const attempts = mock((_field: 'first' | 'second') => {});
+
+ function Display({field}: {field: 'first' | 'second'}) {
+ const value = useClassyStore(store).node[field];
+ attempts(field);
+ if (field === 'second') throw pending;
+ return {value} ;
+ }
+
+ const {root, container} = mount(
+
+
+ ,
+ );
+ await act(async () => {
+ startTransition(() =>
+ root.render(
+
+
+ ,
+ ),
+ );
+ });
+ expect(attempts).toHaveBeenCalledWith('second');
+ await act(async () => {
+ store.node.first = 'Committed update';
+ });
+ expect(container.textContent).toBe('Committed update');
+ act(() =>
+ root.render(
+
+
+ ,
+ ),
+ );
+ attempts.mockClear();
+ await act(async () => {
+ store.node.second = 'Discarded branch';
+ });
+ expect(attempts).not.toHaveBeenCalled();
+ await act(async () => {
+ store.node.first = 'Still current';
+ });
+ expect(container.textContent).toBe('Still current');
+ });
+
+ test('keeps a memoized child live after its parent renders with unchanged data', async () => {
+ const store = createClassyStore({node: {name: 'First', unrelated: 0}});
+ const childRenders = mock(() => {});
+ const Child = memo(function Child({node}: {node: {readonly name: string}}) {
+ childRenders();
+ return {node.name} ;
+ });
+
+ function Display({label}: {label: string}) {
+ const state = useClassyStore(store);
+ return (
+
+ {label}:
+
+ );
+ }
+
+ const {root, container} = mount( );
+ act(() => root.render( ));
+ expect(childRenders).toHaveBeenCalledTimes(1);
+ await act(async () => {
+ store.node.name = 'Store update';
+ });
+ expect(container.textContent).toBe('Parent update:Store update');
+ expect(childRenders).toHaveBeenCalledTimes(2);
+ });
+
+ test('tracks nested object values held by class symbol fields', () => {
+ const key = Symbol('state');
+ class SymbolStore {
+ [key] = {name: 'First'};
+ }
+ const store = createClassyStore(new SymbolStore());
+
+ function Display() {
+ const state = useClassyStore(store);
+ return {state[key].name} ;
+ }
+
+ const {container} = mount( );
+ expect(container.textContent).toBe('First');
+ act(() => {
+ store[key].name = 'Updated';
+ });
+ expect(container.textContent).toBe('Updated');
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/react.ssr.test.tsx b/packages/classy-store/src/frameworks/react/react.ssr.test.tsx
new file mode 100644
index 0000000..b592469
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/react.ssr.test.tsx
@@ -0,0 +1,189 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {act, useEffect} from 'react';
+import {hydrateRoot, type Root} from 'react-dom/client';
+import {renderToString} from 'react-dom/server';
+import {createClassyStore} from '../../core/core';
+import {snapshot} from '../../snapshot/snapshot';
+import {persist} from '../../utils/persist/persist';
+import {createStoreHook, useClassyStore} from './react';
+
+class RequestStore {
+ constructor(
+ public user: {name: string},
+ public count = 0,
+ ) {}
+
+ increment() {
+ this.count++;
+ }
+}
+
+type HydrationViewProps = {
+ store: RequestStore;
+ useBoundStore: ReturnType>;
+ onMount?: () => undefined | (() => void);
+};
+
+function HydrationView({store, useBoundStore, onMount}: HydrationViewProps) {
+ const tracked = useClassyStore(store);
+ const selected = useClassyStore(
+ store,
+ (state) => `${state.user.name}:${state.count}`,
+ );
+ const bound = useBoundStore((state) => state.count);
+ useEffect(() => onMount?.(), [onMount]);
+
+ return (
+
+ {`${tracked.user.name}:${tracked.count}`}
+ {selected}
+ {bound}
+
+ );
+}
+
+const mounted: {root: Root; container: HTMLDivElement}[] = [];
+
+afterEach(async () => {
+ for (const {root, container} of mounted.splice(0)) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+});
+
+function renderRequest(store: RequestStore): string {
+ return renderToString(
+ ,
+ );
+}
+
+describe('React server rendering and hydration', () => {
+ test('keeps server requests isolated', () => {
+ const firstRequest = createClassyStore(
+ new RequestStore({name: 'Alice'}, 2),
+ );
+ const secondRequest = createClassyStore(new RequestStore({name: 'Bob'}, 7));
+
+ const firstHtml = renderRequest(firstRequest);
+ const secondHtml = renderRequest(secondRequest);
+ firstRequest.user.name = 'Updated Alice';
+ firstRequest.increment();
+
+ expect(firstHtml).toContain('Alice:2');
+ expect(secondHtml).toContain('Bob:7');
+ expect(renderRequest(secondRequest)).toBe(secondHtml);
+ expect(secondRequest.count).toBe(7);
+ expect(secondRequest.user.name).toBe('Bob');
+ });
+
+ test('hydrates serialized initial data without replacing server DOM', async () => {
+ const serverStore = createClassyStore(new RequestStore({name: 'Alice'}, 2));
+ const initialData = JSON.stringify(snapshot(serverStore));
+ const container = document.createElement('div');
+ container.innerHTML = renderRequest(serverStore);
+ document.body.appendChild(container);
+ const serverElement = container.firstElementChild;
+
+ const parsed: {user: {name: string}; count: number} =
+ JSON.parse(initialData);
+ const clientStore = createClassyStore(
+ new RequestStore(parsed.user, parsed.count),
+ );
+ const onRecoverableError = mock(() => {});
+
+ await act(async () => {
+ const root = hydrateRoot(
+ container,
+ ,
+ {onRecoverableError},
+ );
+ mounted.push({root, container});
+ });
+
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.firstElementChild).toBe(serverElement);
+ expect(container.textContent).toBe('Alice:2Alice:22');
+
+ act(() => {
+ clientStore.user.name = 'Alicia';
+ clientStore.increment();
+ });
+
+ expect(container.textContent).toBe('Alicia:3Alicia:33');
+ expect(serverStore.user.name).toBe('Alice');
+ expect(serverStore.count).toBe(2);
+ });
+
+ test('defers persisted data until after hydration and cleans up on unmount', async () => {
+ const serverStore = createClassyStore(
+ new RequestStore({name: 'Server'}, 1),
+ );
+ const clientStore = createClassyStore(
+ new RequestStore({name: 'Server'}, 1),
+ );
+ const container = document.createElement('div');
+ container.innerHTML = renderRequest(serverStore);
+ document.body.appendChild(container);
+ const serverElement = container.firstElementChild;
+ const onRecoverableError = mock(() => {});
+
+ let resolveSavedState: (value: string) => void = () => {};
+ const savedState = new Promise((resolve) => {
+ resolveSavedState = resolve;
+ });
+ const storage = {
+ getItem: mock(() => savedState),
+ setItem: mock(() => {}),
+ removeItem: mock(() => {}),
+ };
+ let rehydration: Promise | undefined;
+ const onMount = () => {
+ const persistence = persist(clientStore, {
+ name: 'request-store',
+ storage,
+ skipHydration: true,
+ });
+ rehydration = persistence.rehydrate();
+ return () => persistence.unsubscribe();
+ };
+
+ expect(storage.getItem).not.toHaveBeenCalled();
+ await act(async () => {
+ const root = hydrateRoot(
+ container,
+ ,
+ {onRecoverableError},
+ );
+ mounted.push({root, container});
+ });
+
+ expect(container.firstElementChild).toBe(serverElement);
+ expect(container.textContent).toBe('Server:1Server:11');
+ expect(storage.getItem).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveSavedState(
+ JSON.stringify({version: 0, state: {user: {name: 'Saved'}, count: 8}}),
+ );
+ await rehydration;
+ });
+
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.textContent).toBe('Saved:8Saved:88');
+ expect(storage.setItem).not.toHaveBeenCalled();
+
+ await act(async () => mounted[0].root.unmount());
+ mounted.splice(0);
+ container.remove();
+ clientStore.increment();
+ await Promise.resolve();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/react.test.tsx b/packages/classy-store/src/frameworks/react/react.test.tsx
index 68d7bc0..e7954a4 100644
--- a/packages/classy-store/src/frameworks/react/react.test.tsx
+++ b/packages/classy-store/src/frameworks/react/react.test.tsx
@@ -1,6 +1,6 @@
import {afterEach, describe, expect, it, mock} from 'bun:test';
import {act, type ReactNode} from 'react';
-import {createRoot} from 'react-dom/client';
+import {createRoot, type Root} from 'react-dom/client';
import {createClassyStore} from '../../core/core';
import {createStoreHook, useClassyStore, useLocalStore} from './react';
@@ -10,6 +10,7 @@ import {createStoreHook, useClassyStore, useLocalStore} from './react';
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
let container: HTMLDivElement;
+const mountedRoots = new Set();
function setup(): void {
container = document.createElement('div');
@@ -17,12 +18,15 @@ function setup(): void {
}
function teardown(): void {
- document.body.removeChild(container);
+ for (const root of mountedRoots) act(() => root.unmount());
+ mountedRoots.clear();
+ container.remove();
}
/** Render a component and return a helper to update. */
function render(element: ReactNode): void {
const root = createRoot(container);
+ mountedRoots.add(root);
act(() => {
root.render(element);
});
@@ -79,7 +83,7 @@ describe('useClassyStore β selector mode', () => {
expect(renderCount).toHaveBeenCalledTimes(2);
});
- it('supports sync selector subscriptions', () => {
+ it('uses synchronous selector subscriptions by default', () => {
class Counter {
count = 0;
increment() {
@@ -90,9 +94,7 @@ describe('useClassyStore β selector mode', () => {
const renderCount = mock(() => {});
function Display() {
- const count = useClassyStore(store, (state) => state.count, {
- sync: true,
- });
+ const count = useClassyStore(store, (state) => state.count);
renderCount();
return {count}
;
}
@@ -291,12 +293,12 @@ describe('useClassyStore β auto-tracked mode', () => {
expect(renderCount).toHaveBeenCalledTimes(2);
});
- it('supports sync auto-tracked subscriptions', () => {
+ it('uses synchronous auto-tracked subscriptions by default', () => {
const store = createClassyStore({count: 0, name: 'hello'});
const renderCount = mock(() => {});
function Display() {
- const snap = useClassyStore(store, {sync: true});
+ const snap = useClassyStore(store);
renderCount();
return {snap.count}
;
}
@@ -565,15 +567,13 @@ describe('useLocalStore', () => {
expect(container.textContent).toBe('world');
expect(renderCount).toHaveBeenCalledTimes(2);
- // Change count β NOT accessed by component, but auto-tracked mode
- // re-renders because the snapshot reference changes on any mutation.
- // (Documented behavior β see "Set-then-revert" in TUTORIAL.md.)
+ // Change count β not read during render, so no update is needed.
await act(async () => {
storeRef.count = 99;
await flush();
});
- expect(renderCount).toHaveBeenCalledTimes(3);
+ expect(renderCount).toHaveBeenCalledTimes(2);
});
});
@@ -665,6 +665,111 @@ describe('useClassyStore β multiple stores', () => {
describe('useClassyStore β selector edge cases', () => {
afterEach(teardown);
+ it.each([
+ {description: 'direct hook', bound: false},
+ {description: 'bound hook', bound: true},
+ ])('updates prop-dependent selectors without a store mutation ($description)', async ({
+ bound,
+ }) => {
+ const store = createClassyStore({names: ['Alice', 'Bob']});
+ const useStore = createStoreHook(store);
+
+ function DirectDisplay({index}: {index: number}) {
+ const name = useClassyStore(store, (state) => state.names[index]);
+ return {name}
;
+ }
+
+ function BoundDisplay({index}: {index: number}) {
+ const name = useStore((state) => state.names[index]);
+ return {name}
;
+ }
+
+ const Display = bound ? BoundDisplay : DirectDisplay;
+ setup();
+ const root = createRoot(container);
+
+ try {
+ act(() => root.render( ));
+ expect(container.textContent).toBe('Alice');
+
+ act(() => root.render( ));
+ expect(container.textContent).toBe('Bob');
+
+ await act(async () => {
+ store.names[1] = 'Bobby';
+ await flush();
+ });
+ expect(container.textContent).toBe('Bobby');
+
+ act(() => root.render( ));
+ expect(container.textContent).toBe('Alice');
+ } finally {
+ act(() => root.unmount());
+ }
+ });
+
+ it('reevaluates the selection when the equality function changes', async () => {
+ const store = createClassyStore({count: 0});
+ const selector = (state: {readonly count: number}) => state.count;
+ const ignoreChanges = () => true;
+
+ function Display({ignore}: {ignore: boolean}) {
+ const count = useClassyStore(store, selector, {
+ isEqual: ignore ? ignoreChanges : Object.is,
+ });
+ return {count}
;
+ }
+
+ setup();
+ const root = createRoot(container);
+
+ try {
+ act(() => root.render( ));
+ await act(async () => {
+ store.count = 1;
+ await flush();
+ });
+ expect(container.textContent).toBe('0');
+
+ act(() => root.render( ));
+ expect(container.textContent).toBe('1');
+ } finally {
+ act(() => root.unmount());
+ }
+ });
+
+ it('preserves equal selection references when the selector changes', () => {
+ const store = createClassyStore({names: ['Alice', 'Alice', 'Bob']});
+ const selections: {name: string}[] = [];
+
+ function Display({index}: {index: number}) {
+ const selected = useClassyStore(
+ store,
+ (state) => ({name: state.names[index]}),
+ {isEqual: (previous, next) => previous.name === next.name},
+ );
+ selections.push(selected);
+ return {selected.name}
;
+ }
+
+ setup();
+ const root = createRoot(container);
+
+ try {
+ act(() => root.render( ));
+ const initialSelection = selections.at(-1);
+
+ act(() => root.render( ));
+ expect(selections.at(-1)).toBe(initialSelection);
+
+ act(() => root.render( ));
+ expect(container.textContent).toBe('Bob');
+ expect(selections.at(-1)).not.toBe(initialSelection);
+ } finally {
+ act(() => root.unmount());
+ }
+ });
+
it('handles selector returning undefined', () => {
const s = createClassyStore({data: null as string | null});
@@ -741,7 +846,7 @@ describe('useClassyStore β selector edge cases', () => {
it('selector returning undefined twice in a row does not over-invoke selector', async () => {
// Pre-fix, the fast path used `resultRef.current !== undefined` which
// skipped memoization whenever the selector legitimately returned
- // `undefined`. With `hasResultRef` the fast path engages.
+ // `undefined`. A cached selection must distinguish it from a cache miss.
const s = createClassyStore({maybe: undefined as string | undefined});
const renderCount = mock(() => {});
const selector = mock((state: {maybe: string | undefined}) => state.maybe);
@@ -849,7 +954,7 @@ describe('createStoreHook', () => {
expect(container.textContent).toBe('1');
});
- it('supports sync selector options', () => {
+ it('uses synchronous selector subscriptions by default', () => {
class Counter {
count = 0;
increment() {
@@ -860,7 +965,7 @@ describe('createStoreHook', () => {
const useCounter = createStoreHook(store);
function Display() {
- const count = useCounter((state) => state.count, {sync: true});
+ const count = useCounter((state) => state.count);
return {count}
;
}
@@ -900,12 +1005,12 @@ describe('createStoreHook', () => {
expect(renderCount).toHaveBeenCalledTimes(2);
});
- it('supports sync auto-tracked options', () => {
+ it('uses synchronous auto-tracked subscriptions by default', () => {
const store = createClassyStore({count: 0, name: 'hello'});
const useStore = createStoreHook(store);
function Display() {
- const snap = useStore({sync: true});
+ const snap = useStore();
return {snap.count}
;
}
diff --git a/packages/classy-store/src/frameworks/react/react.ts b/packages/classy-store/src/frameworks/react/react.ts
index cf8d46e..5cf651a 100644
--- a/packages/classy-store/src/frameworks/react/react.ts
+++ b/packages/classy-store/src/frameworks/react/react.ts
@@ -1,5 +1,13 @@
+'use client';
+
import {createProxy, isChanged} from 'proxy-compare';
-import {useCallback, useRef, useState, useSyncExternalStore} from 'react';
+import {
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ useSyncExternalStore,
+} from 'react';
import {
subscribe as coreSubscribe,
createClassyStore,
@@ -7,17 +15,22 @@ import {
} from '../../core/core';
import {snapshot} from '../../snapshot/snapshot';
import type {Snapshot} from '../../types';
+import {hydrationSnapshots} from './utils/hydrationSnapshots/hydrationSnapshots';
+import {SnapshotTargetCache} from './utils/snapshotTargetCache/snapshotTargetCache';
+import {TrackingReadMap} from './utils/trackingReadMap/trackingReadMap';
-type EqualityFn = (a: T, b: T) => boolean;
+export {prepareStoreForHydration} from './server';
-export type UseClassyStoreOptions = {
- sync?: boolean;
-};
+type EqualityFn = (a: T, b: T) => boolean;
-export type UseClassyStoreSelectorOptions = UseClassyStoreOptions & {
+export type UseClassyStoreSelectorOptions = {
isEqual?: EqualityFn;
};
+// A frozen snapshot's backing copy is identical for every consumer. Without
+// this cache, each row rescans and copies the entire containing list on access.
+const snapshotTargets = new SnapshotTargetCache();
+
// ββ Overloads βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
@@ -28,7 +41,7 @@ export type UseClassyStoreSelectorOptions = UseClassyStoreOptions & {
*
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
* @param selector - Picks data from the immutable snapshot.
- * @param options - Controls equality and subscriber notification timing.
+ * @param options - Controls selected-value equality.
*/
export function useClassyStore(
proxyStore: T,
@@ -43,166 +56,188 @@ export function useClassyStore(
* The component only re-renders when a property it actually read changes.
*
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
- * @param options - Controls subscriber notification timing.
*/
-export function useClassyStore(
- proxyStore: T,
- options?: UseClassyStoreOptions,
-): Snapshot;
+export function useClassyStore(proxyStore: T): Snapshot;
// ββ Implementation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function useClassyStore(
proxyStore: T,
- selectorOrOptions?: ((snap: Snapshot) => S) | UseClassyStoreOptions,
+ selector?: (snap: Snapshot) => S,
selectorOptions?: UseClassyStoreSelectorOptions,
): Snapshot | S {
// Validate that the argument is actually a store proxy (throws if not).
getInternal(proxyStore);
- const selector =
- typeof selectorOrOptions === 'function' ? selectorOrOptions : undefined;
- const options:
- | UseClassyStoreOptions
- | UseClassyStoreSelectorOptions
- | undefined =
- typeof selectorOrOptions === 'function'
- ? selectorOptions
- : selectorOrOptions;
- const sync = options?.sync === true;
const isEqual = selectorOptions?.isEqual;
- // Stable subscribe function (internal identity never changes for a given store).
- const subscribe = useCallback(
- (onStoreChange: () => void) =>
- coreSubscribe(proxyStore, onStoreChange, sync ? {sync: true} : undefined),
- [proxyStore, sync],
- );
+ // React must observe mutations during the event that changes a controlled
+ // input. Ordinary core subscriptions retain their default microtask batching.
+ const subscribe = useMemo(() => createSubscription(proxyStore), [proxyStore]);
// ββ Refs used by both modes (always allocated to satisfy Rules of Hooks) ββ
- // Selector mode refs
- const snapRef = useRef | undefined>(undefined);
- const resultRef = useRef(undefined);
- // Tracks whether `resultRef` holds a real prior result (so a selector that
- // legitimately returns `undefined` still benefits from the fast path).
- const hasResultRef = useRef(false);
+ // Keep only committed selections shared between selector instances.
+ // Each selector gets its own snapshot cache so concurrent renders do not
+ // overwrite one another's cached snapshot or selected result.
+ const committedSelectionRef = useRef<{value: S} | undefined>(undefined);
+ const initialSnapshot = hydrationSnapshots.get(proxyStore) as
+ | Snapshot
+ | undefined;
+ const {getSelectedSnapshot, getServerSnapshot} = useMemo(
+ () =>
+ createSnapshotGetters(
+ proxyStore,
+ selector,
+ isEqual,
+ committedSelectionRef,
+ initialSnapshot,
+ ),
+ [proxyStore, selector, isEqual, initialSnapshot],
+ );
- // Auto-track mode refs
- const affected = useRef(new WeakMap()).current;
+ // Each render owns its read set, seeded from committed reads of the same
+ // immutable objects. Memoization can skip those reads on subsequent renders.
+ // Weak keys release old dependencies as their snapshots become unreachable.
+ const committedReads = useRef(new WeakMap()).current;
+ const affected = new TrackingReadMap(committedReads);
const proxyCache = useRef(new WeakMap()).current;
- const prevSnapRef = useRef | undefined>(undefined);
- const wrappedRef = useRef | undefined>(undefined);
-
- // ββ Single getSnapshot for useSyncExternalStore βββββββββββββββββββββββββββ
-
- const getSnapshot = (): Snapshot | S =>
- selector
- ? getSelectorSnapshot(
- proxyStore,
- snapRef,
- resultRef,
- hasResultRef,
- selector,
- isEqual,
- )
- : getAutoTrackSnapshot(
- proxyStore,
- affected,
- proxyCache,
- prevSnapRef,
- wrappedRef,
- );
-
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+ let previousAutoSnapshot: Snapshot | undefined;
+ const getSnapshot = (): Snapshot | S => {
+ if (getSelectedSnapshot) return getSelectedSnapshot();
+ const nextSnapshot = snapshot(proxyStore);
+ if (
+ previousAutoSnapshot &&
+ !isChanged(previousAutoSnapshot, nextSnapshot, affected)
+ ) {
+ return previousAutoSnapshot;
+ }
+ previousAutoSnapshot = nextSnapshot;
+ return nextSnapshot;
+ };
+
+ const readServerSnapshot = (): Snapshot | S => {
+ const serverResult = getServerSnapshot
+ ? getServerSnapshot()
+ : getSnapshot();
+ // This render's read set can filter the first live update, too. Seed only
+ // when React hydrates so client-only mounts always start from current data.
+ if (!selector) previousAutoSnapshot = serverResult as Snapshot;
+ return serverResult;
+ };
+
+ const result = useSyncExternalStore(
+ subscribe,
+ getSnapshot,
+ readServerSnapshot,
+ );
+
+ useEffect(() => {
+ committedSelectionRef.current = selector ? {value: result as S} : undefined;
+ affected.commit();
+ }, [result, selector, affected]);
+
+ return selector
+ ? result
+ : (createProxy(
+ result as Snapshot,
+ affected,
+ proxyCache,
+ snapshotTargets,
+ ) as Snapshot);
}
// ββ Selector mode logic (pure function, no hooks) βββββββββββββββββββββββββββββ
-/**
- * `getSnapshot` implementation for selector mode.
- *
- * Fast-paths when the snapshot reference hasn't changed (O(1)). Otherwise
- * runs the selector against the new snapshot and compares the result to the
- * previous one via `Object.is` (or `options.isEqual`). Returns the previous
- * result reference when equal, preventing unnecessary React re-renders.
- *
- * Pure function -- no hooks, safe to call from `useSyncExternalStore`.
- */
-function getSelectorSnapshot(
- proxyStore: T,
- snapRef: React.RefObject | undefined>,
- resultRef: React.RefObject,
- hasResultRef: React.RefObject,
- selector: (snap: Snapshot) => S,
- isEqual?: EqualityFn,
-): S {
- const nextSnap = snapshot(proxyStore);
-
- // Fast path: same snapshot reference β same result.
- if (snapRef.current === nextSnap && hasResultRef.current) {
- return resultRef.current as S;
- }
+// Construct stable callbacks outside the hook's lexical scope. Engines can
+// share a closure context with render-local values, otherwise keeping the first
+// result and auto-tracked snapshot alive for the entire mounted subscription.
+function createSubscription(proxyStore: T) {
+ return (onStoreChange: () => void) =>
+ coreSubscribe(proxyStore, onStoreChange, {sync: true});
+}
- const nextResult = selector(nextSnap);
- snapRef.current = nextSnap;
-
- // Check equality with previous result.
- if (
- hasResultRef.current &&
- (isEqual
- ? isEqual(resultRef.current as S, nextResult)
- : Object.is(resultRef.current as S, nextResult))
- ) {
- return resultRef.current as S;
- }
+function createSnapshotGetters(
+ proxyStore: T,
+ selector: ((snap: Snapshot) => S) | undefined,
+ isEqual: EqualityFn | undefined,
+ committedSelectionRef: {current: {value: S} | undefined},
+ initialSnapshot: Snapshot | undefined,
+) {
+ let hydrationSelection: {value: S; snapshot: Snapshot} | undefined;
+ const readServerSelection =
+ initialSnapshot && selector
+ ? createSelectorSnapshotGetter(
+ () => initialSnapshot,
+ selector,
+ () => undefined,
+ isEqual,
+ )
+ : undefined;
- resultRef.current = nextResult;
- hasResultRef.current = true;
- return nextResult;
+ return {
+ getSelectedSnapshot: selector
+ ? createSelectorSnapshotGetter(
+ () => snapshot(proxyStore),
+ selector,
+ () => hydrationSelection ?? committedSelectionRef.current,
+ isEqual,
+ )
+ : undefined,
+ getServerSnapshot: initialSnapshot
+ ? () => {
+ if (!readServerSelection) return initialSnapshot;
+ const value = readServerSelection();
+ // Seed only this selector's live cache, and only when React actually
+ // hydrates. Never overwrite committed data from an abandoned render.
+ hydrationSelection = {value, snapshot: initialSnapshot};
+ return value;
+ }
+ : undefined,
+ };
}
-// ββ Auto-tracked (selectorless) mode logic (pure function, no hooks) ββββββββββ
-
/**
- * `getSnapshot` implementation for auto-tracked (selectorless) mode.
- *
- * Uses `proxy-compare` to diff only the properties the component actually read.
- * If the snapshot reference is the same, returns the cached tracking proxy.
- * If the snapshot changed but no tracked property differs (`isChanged` returns
- * false), also returns the cached proxy -- avoiding re-render. Only when a
- * relevant property changed does it create a new `createProxy` wrapper.
+ * Create a cached `getSnapshot` for one store, selector, and equality function.
*
- * Pure function -- no hooks, safe to call from `useSyncExternalStore`.
+ * Re-evaluates when any of those inputs change, even if the store did not.
+ * Repeated reads of the same snapshot return the cached result, including
+ * `undefined`. Equal selections retain their reference across selector changes.
*/
-function getAutoTrackSnapshot(
- proxyStore: T,
- affected: WeakMap,
- proxyCache: WeakMap,
- prevSnapRef: React.RefObject | undefined>,
- wrappedRef: React.RefObject | undefined>,
-): Snapshot {
- const nextSnap = snapshot(proxyStore);
-
- // If the raw snapshot is the same reference, nothing changed.
- if (prevSnapRef.current === nextSnap) {
- return wrappedRef.current as Snapshot;
- }
-
- // Check if any property the component actually read has changed.
- if (
- prevSnapRef.current !== undefined &&
- !isChanged(prevSnapRef.current, nextSnap, affected)
- ) {
- // No property the component cares about changed β return same wrapped proxy.
- return wrappedRef.current as Snapshot;
- }
-
- // Something relevant changed β create a new tracking proxy.
- prevSnapRef.current = nextSnap;
- const wrapped = createProxy(nextSnap, affected, proxyCache) as Snapshot;
- wrappedRef.current = wrapped;
- return wrapped;
+function createSelectorSnapshotGetter(
+ getStoreSnapshot: () => Snapshot,
+ selector: (snap: Snapshot) => S,
+ getPreviousSelection: () => {value: S; snapshot?: Snapshot} | undefined,
+ isEqual?: EqualityFn,
+): () => S {
+ let previousSnapshot: Snapshot | undefined;
+ let selection: {value: S; snapshot?: Snapshot} | undefined;
+
+ return () => {
+ const nextSnapshot = getStoreSnapshot();
+ if (previousSnapshot === nextSnapshot && selection) {
+ return selection.value;
+ }
+
+ const previousSelection = selection ?? getPreviousSelection();
+ // The same immutable input and selector can reuse the hydration result
+ // directly, including selectors that allocate an object without isEqual.
+ const nextResult =
+ !selection && previousSelection?.snapshot === nextSnapshot
+ ? previousSelection.value
+ : selector(nextSnapshot);
+
+ selection =
+ previousSelection &&
+ (isEqual
+ ? isEqual(previousSelection.value, nextResult)
+ : Object.is(previousSelection.value, nextResult))
+ ? previousSelection
+ : {value: nextResult};
+ previousSnapshot = nextSnapshot;
+
+ return selection.value;
+ };
}
// ββ Bound store hook factory βββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -234,18 +269,18 @@ export function createStoreHook(proxyStore: T) {
// Fail fast at creation time rather than on first render.
getInternal(proxyStore);
- function useStore(options?: UseClassyStoreOptions): Snapshot;
+ function useStore(): Snapshot;
function useStore(
selector: (snap: Snapshot) => S,
options?: UseClassyStoreSelectorOptions,
): S;
function useStore(
- selectorOrOptions?: ((snap: Snapshot) => S) | UseClassyStoreOptions,
+ selector?: (snap: Snapshot) => S,
options?: UseClassyStoreSelectorOptions,
): Snapshot | S {
return useClassyStore(
proxyStore,
- selectorOrOptions as (snap: Snapshot) => S,
+ selector as (snap: Snapshot) => S,
options,
);
}
@@ -256,17 +291,19 @@ export function createStoreHook(proxyStore: T) {
/**
* Create a component-scoped reactive store that lives for the lifetime of the
- * component. When the component unmounts, the store becomes unreferenced and is
- * garbage collected (all internal bookkeeping uses `WeakMap`).
+ * component. Unmounting releases React's reference; the store can be collected
+ * when no external references remain. Clean up application subscriptions and
+ * other effects separately; this hook does not call a store's dispose method.
*
- * The factory function runs **once** per mount (via `useState` initializer).
+ * The factory is a `useState` initializer and must be pure. React StrictMode
+ * may invoke it more than once in development.
* Each component instance gets its own isolated store.
*
* Use the returned proxy with `useClassyStore()` to read state in the same component
* or pass it down via props/context to share within a subtree.
*
* @param factory - A function that returns a class instance (or plain object).
- * Called once per component mount.
+ * Used as the component state initializer.
* @returns A reactive store proxy scoped to the component's lifetime.
*
* @example
diff --git a/packages/classy-store/src/frameworks/react/react.utilities.test.tsx b/packages/classy-store/src/frameworks/react/react.utilities.test.tsx
new file mode 100644
index 0000000..b611760
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/react.utilities.test.tsx
@@ -0,0 +1,238 @@
+import {afterEach, beforeEach, describe, expect, mock, test} from 'bun:test';
+import {act} from 'react';
+import {createRoot} from 'react-dom/client';
+import {
+ type ReactiveMap,
+ type ReactiveSet,
+ reactiveMap,
+ reactiveSet,
+} from '../../collections/index';
+import {createClassyStore, snapshot, subscribe} from '../../index';
+import {
+ devtools,
+ type PersistOptions,
+ persist,
+ withHistory,
+} from '../../utils/index';
+import {UtilityValue} from './testHelpers/utilityValue/UtilityValue';
+
+const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
+const cleanups: Array<() => void> = [];
+let previousExtension: Window['__REDUX_DEVTOOLS_EXTENSION__'];
+
+function disposeAll() {
+ for (const cleanup of cleanups.splice(0).reverse()) cleanup();
+}
+
+beforeEach(() => {
+ previousExtension = window.__REDUX_DEVTOOLS_EXTENSION__;
+});
+
+afterEach(async () => {
+ disposeAll();
+ await flush();
+ if (previousExtension) {
+ window.__REDUX_DEVTOOLS_EXTENSION__ = previousExtension;
+ } else {
+ delete window.__REDUX_DEVTOOLS_EXTENSION__;
+ }
+});
+
+function createStorage() {
+ const data = new Map();
+ return {
+ data,
+ getItem: (name: string) => data.get(name) ?? null,
+ setItem: mock((name: string, value: string) => {
+ data.set(name, value);
+ }),
+ removeItem: (name: string) => {
+ data.delete(name);
+ },
+ };
+}
+
+function installDevtools() {
+ type Extension = NonNullable;
+ type Connection = ReturnType;
+ type Listener = Parameters[0];
+ let listener: Listener | undefined;
+ const unsubscribe = mock(() => {
+ listener = undefined;
+ });
+ const connection = {
+ init: mock((_state: unknown) => {}),
+ send: mock((_action: string | {type: string}, _state: unknown) => {}),
+ subscribe: (nextListener: Listener) => {
+ listener = nextListener;
+ return unsubscribe;
+ },
+ };
+ window.__REDUX_DEVTOOLS_EXTENSION__ = {connect: () => connection};
+ return {
+ connection,
+ unsubscribe,
+ jumpTo(state: {count: number}) {
+ if (!listener) throw new Error('DevTools must be connected');
+ listener({
+ type: 'DISPATCH',
+ payload: {type: 'JUMP_TO_STATE'},
+ state: JSON.stringify(state),
+ });
+ },
+ };
+}
+
+describe('core and React utility happy paths', () => {
+ test('persists edits, undo and redo, then restores the final state in a new store', async () => {
+ const storage = createStorage();
+ const store = createClassyStore({count: 0});
+ const persistence = persist(store, {name: 'draft', storage});
+ cleanups.push(persistence.unsubscribe);
+ await persistence.hydrated;
+ const history = withHistory(store);
+ cleanups.push(history.dispose);
+
+ store.count = 1;
+ await flush();
+ expect(JSON.parse(storage.data.get('draft') ?? '').state.count).toBe(1);
+
+ history.undo();
+ await flush();
+ expect(store.count).toBe(0);
+ expect(JSON.parse(storage.data.get('draft') ?? '').state.count).toBe(0);
+
+ history.redo();
+ await flush();
+ expect(store.count).toBe(1);
+ expect(JSON.parse(storage.data.get('draft') ?? '').state.count).toBe(1);
+ disposeAll();
+
+ const restored = createClassyStore({count: 0});
+ const restoredPersistence = persist(restored, {name: 'draft', storage});
+ cleanups.push(restoredPersistence.unsubscribe);
+ await restoredPersistence.hydrated;
+ expect(restored.count).toBe(1);
+ });
+
+ test('sends edits to DevTools and renders time-travel state in React', async () => {
+ const extension = installDevtools();
+ const store = createClassyStore({count: 0});
+ cleanups.push(devtools(store));
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ cleanups.push(() => {
+ act(() => root.unmount());
+ container.remove();
+ });
+ act(() => root.render( ));
+ expect(container.textContent).toBe('0');
+ expect(extension.connection.init).toHaveBeenCalledWith({count: 0});
+
+ await act(async () => {
+ store.count = 3;
+ });
+ expect(container.textContent).toBe('3');
+ expect(extension.connection.send).toHaveBeenCalledWith(
+ {type: 'STORE_UPDATE'},
+ {count: 3},
+ );
+ extension.connection.send.mockClear();
+
+ await act(async () => extension.jumpTo({count: 1}));
+ expect(store.count).toBe(1);
+ expect(container.textContent).toBe('1');
+ expect(extension.connection.send).not.toHaveBeenCalled();
+ });
+
+ test('restores reactive Map and Set transforms and keeps subsequent edits reactive', async () => {
+ const storage = createStorage();
+ const createStore = () =>
+ createClassyStore({
+ users: reactiveMap(),
+ tags: reactiveSet(),
+ });
+ const store = createStore();
+ const options: PersistOptions = {
+ name: 'collections',
+ storage,
+ properties: [
+ {
+ key: 'users',
+ serialize: (value) =>
+ Array.from((value as ReactiveMap).entries()),
+ deserialize: (value) => reactiveMap(value as Array<[string, string]>),
+ },
+ {
+ key: 'tags',
+ serialize: (value) =>
+ Array.from((value as ReactiveSet).values()),
+ deserialize: (value) => reactiveSet(value as string[]),
+ },
+ ],
+ };
+ const persistence = persist(store, options);
+ cleanups.push(persistence.unsubscribe);
+ await persistence.hydrated;
+ store.users.set('ada', 'Ada');
+ store.tags.add('saved');
+ await flush();
+ disposeAll();
+
+ const restored = createStore();
+ const restoredPersistence = persist(restored, options);
+ cleanups.push(restoredPersistence.unsubscribe);
+ await restoredPersistence.hydrated;
+ expect(restored.users.get('ada')).toBe('Ada');
+ expect(restored.tags.has('saved')).toBe(true);
+ const before = snapshot(restored);
+ const onChange = mock(() => {});
+ cleanups.push(subscribe(restored, onChange));
+
+ restored.users.set('grace', 'Grace');
+ restored.tags.add('edited');
+ await flush();
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(snapshot(restored).users.get('grace')).toBe('Grace');
+ expect(snapshot(restored).tags.has('edited')).toBe(true);
+ expect(before.users.has('grace')).toBe(false);
+ expect(before.tags.has('edited')).toBe(false);
+ expect(JSON.parse(storage.data.get('collections') ?? '').state).toEqual({
+ users: [
+ ['ada', 'Ada'],
+ ['grace', 'Grace'],
+ ],
+ tags: ['saved', 'edited'],
+ });
+ });
+
+ test('disposes persistence, history and DevTools together without further side effects', async () => {
+ const storage = createStorage();
+ const extension = installDevtools();
+ const store = createClassyStore({count: 0});
+ const persistence = persist(store, {name: 'draft', storage});
+ cleanups.push(persistence.unsubscribe);
+ await persistence.hydrated;
+ const history = withHistory(store);
+ cleanups.push(history.dispose, devtools(store));
+
+ store.count = 1;
+ await flush();
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+ expect(extension.connection.send).toHaveBeenCalledTimes(1);
+ expect(history.canUndo).toBe(true);
+ disposeAll();
+ storage.setItem.mockClear();
+ extension.connection.send.mockClear();
+
+ store.count = 2;
+ await flush();
+ expect(store.count).toBe(2);
+ expect(storage.setItem).not.toHaveBeenCalled();
+ expect(extension.connection.send).not.toHaveBeenCalled();
+ expect(extension.unsubscribe).toHaveBeenCalledTimes(1);
+ expect(history.canUndo).toBe(false);
+ expect(history.canRedo).toBe(false);
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/server.ts b/packages/classy-store/src/frameworks/react/server.ts
new file mode 100644
index 0000000..8f86bce
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/server.ts
@@ -0,0 +1,24 @@
+import {getInternal} from '../../core/core';
+import {snapshot} from '../../snapshot/snapshot';
+import {hydrationSnapshots} from './utils/hydrationSnapshots/hydrationSnapshots';
+
+/**
+ * Capture the initial snapshot used for server rendering and client hydration.
+ *
+ * Call on each request's store and on its client counterpart, initialized with
+ * the same data, before rendering or starting persistence. Repeated calls keep
+ * the first snapshot. The returned proxy is unchanged and remains mutable.
+ * Client-only stores do not need preparation.
+ *
+ * Prepare the exact proxy passed to the hooks. Opaque snapshot leaves such as
+ * Date and Map retain their identity; replace them instead of mutating in place.
+ * This entry imports no React hooks and is safe in a React Server Component.
+ * Pass serializable initial data across that boundary, not the store instance.
+ */
+export function prepareStoreForHydration(proxyStore: T): T {
+ getInternal(proxyStore);
+ if (!hydrationSnapshots.has(proxyStore)) {
+ hydrationSnapshots.set(proxyStore, snapshot(proxyStore));
+ }
+ return proxyStore;
+}
diff --git a/packages/classy-store/src/frameworks/react/testHelpers/delayedHydrationView/DelayedHydrationView.tsx b/packages/classy-store/src/frameworks/react/testHelpers/delayedHydrationView/DelayedHydrationView.tsx
new file mode 100644
index 0000000..6b7b1ba
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/testHelpers/delayedHydrationView/DelayedHydrationView.tsx
@@ -0,0 +1,22 @@
+import {useEffect} from 'react';
+import type {
+ HydrationGate,
+ HydrationStore,
+ UseHydrationValue,
+} from '../hydration.types';
+
+type Props = {
+ gate: HydrationGate;
+ store: HydrationStore;
+ useValue: UseHydrationValue;
+ onCommit: (value: string) => void;
+};
+
+export function DelayedHydrationView({gate, store, useValue, onCommit}: Props) {
+ gate.read();
+ const value = useValue(store);
+ useEffect(() => {
+ onCommit(value);
+ }, [onCommit, value]);
+ return {value} ;
+}
diff --git a/packages/classy-store/src/frameworks/react/testHelpers/hydration.types.ts b/packages/classy-store/src/frameworks/react/testHelpers/hydration.types.ts
new file mode 100644
index 0000000..80ac40d
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/testHelpers/hydration.types.ts
@@ -0,0 +1,8 @@
+export type HydrationStore = {user: {name: string}; count: number};
+
+export type HydrationGate = {
+ read: () => void;
+ release: () => void;
+};
+
+export type UseHydrationValue = (store: HydrationStore) => string;
diff --git a/packages/classy-store/src/frameworks/react/testHelpers/performanceQa.types.ts b/packages/classy-store/src/frameworks/react/testHelpers/performanceQa.types.ts
new file mode 100644
index 0000000..7a93c87
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/testHelpers/performanceQa.types.ts
@@ -0,0 +1,11 @@
+import type {Snapshot} from '../../../types';
+
+export type PerformanceQaStore = {
+ rows: {name: string; ignored: number}[];
+ ignored: number;
+};
+
+export type PerformanceQaCommit = (
+ id: string,
+ state: Snapshot,
+) => void;
diff --git a/packages/classy-store/src/frameworks/react/testHelpers/performanceQaComputedView/PerformanceQaComputedView.tsx b/packages/classy-store/src/frameworks/react/testHelpers/performanceQaComputedView/PerformanceQaComputedView.tsx
new file mode 100644
index 0000000..00713d4
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/testHelpers/performanceQaComputedView/PerformanceQaComputedView.tsx
@@ -0,0 +1,15 @@
+import {useClassyStore} from '../../react';
+
+type Props = {
+ store: {derived: {child: {name: string}}};
+ label: string;
+};
+
+export function PerformanceQaComputedView({store, label}: Props) {
+ const state = useClassyStore(store);
+ return (
+
+ {label}:{state.derived.child.name}
+
+ );
+}
diff --git a/packages/classy-store/src/frameworks/react/testHelpers/performanceQaView/PerformanceQaView.tsx b/packages/classy-store/src/frameworks/react/testHelpers/performanceQaView/PerformanceQaView.tsx
new file mode 100644
index 0000000..c068102
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/testHelpers/performanceQaView/PerformanceQaView.tsx
@@ -0,0 +1,33 @@
+import {useEffect} from 'react';
+import {useClassyStore} from '../../react';
+import type {
+ PerformanceQaCommit,
+ PerformanceQaStore,
+} from '../performanceQa.types';
+
+type Props = {
+ id: string;
+ store: PerformanceQaStore;
+ index: number;
+ label?: string;
+ onCommit: PerformanceQaCommit;
+};
+
+export function PerformanceQaView({
+ id,
+ store,
+ index,
+ label = '',
+ onCommit,
+}: Props) {
+ const state = useClassyStore(store);
+ const name = state.rows[index].name;
+ useEffect(() => {
+ onCommit(id, state);
+ });
+ return (
+
+ {label}:{name}
+
+ );
+}
diff --git a/packages/classy-store/src/frameworks/react/testHelpers/utilityValue/UtilityValue.tsx b/packages/classy-store/src/frameworks/react/testHelpers/utilityValue/UtilityValue.tsx
new file mode 100644
index 0000000..2b82c77
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/testHelpers/utilityValue/UtilityValue.tsx
@@ -0,0 +1,6 @@
+import {useClassyStore} from '../../react';
+
+export function UtilityValue({store}: {store: {count: number}}) {
+ const state = useClassyStore(store);
+ return {state.count} ;
+}
diff --git a/packages/classy-store/src/frameworks/react/utils/hydrationSnapshots/hydrationSnapshots.ts b/packages/classy-store/src/frameworks/react/utils/hydrationSnapshots/hydrationSnapshots.ts
new file mode 100644
index 0000000..da3fb52
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/utils/hydrationSnapshots/hydrationSnapshots.ts
@@ -0,0 +1,4 @@
+// Shared by the server-safe preparation entry and the client hooks in each
+// module graph. A late Suspense boundary may need initial data after an earlier
+// consumer has hydrated and changed the live store.
+export const hydrationSnapshots = new WeakMap();
diff --git a/packages/classy-store/src/frameworks/react/utils/snapshotTargetCache/snapshotTargetCache.test.ts b/packages/classy-store/src/frameworks/react/utils/snapshotTargetCache/snapshotTargetCache.test.ts
new file mode 100644
index 0000000..b8301fa
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/utils/snapshotTargetCache/snapshotTargetCache.test.ts
@@ -0,0 +1,85 @@
+import {describe, expect, spyOn, test} from 'bun:test';
+import {createProxy, isChanged} from 'proxy-compare';
+import {SnapshotTargetCache} from './snapshotTargetCache';
+
+describe('SnapshotTargetCache', () => {
+ test('scans a shared frozen list once across hundreds of independent readers', () => {
+ const items = Object.freeze(
+ Array.from({length: 1000}, (_, index) => Object.freeze({value: index})),
+ );
+ const source = Object.freeze({items});
+ const cache = new SnapshotTargetCache();
+ const descriptors = Object.getOwnPropertyDescriptors;
+ let listScans = 0;
+ const descriptorSpy = spyOn(
+ Object,
+ 'getOwnPropertyDescriptors',
+ ).mockImplementation((target) => {
+ if (target === items) listScans += 1;
+ return descriptors(target);
+ });
+ try {
+ for (let index = 0; index < 1000; index += 1) {
+ const reads = new WeakMap();
+ const proxyCache = new WeakMap();
+ const tracked = createProxy(source, reads, proxyCache, cache);
+ expect(tracked.items[index].value).toBe(index);
+ expect(createProxy(source, reads, proxyCache, cache)).toBe(tracked);
+ }
+ expect(listScans).toBe(1);
+ } finally {
+ descriptorSpy.mockRestore();
+ }
+ });
+
+ test('keeps reader dependencies separate while sharing backing copies', () => {
+ const source = Object.freeze({first: 1, second: 2});
+ const cache = new SnapshotTargetCache();
+ const firstReads = new WeakMap();
+ const secondReads = new WeakMap();
+ const first = createProxy(source, firstReads, new WeakMap(), cache);
+ const second = createProxy(source, secondReads, new WeakMap(), cache);
+ expect(first.first).toBe(1);
+ expect(second.second).toBe(2);
+ const next = Object.freeze({first: 3, second: 2});
+ expect(isChanged(source, next, firstReads)).toBe(true);
+ expect(isChanged(source, next, secondReads)).toBe(false);
+ });
+
+ test('protects both the initial and cached copies against reflective mutation', () => {
+ const source = Object.freeze({nested: Object.freeze({value: 1})});
+ const cache = new SnapshotTargetCache();
+ const first = createProxy(source, new WeakMap(), new WeakMap(), cache);
+ const second = createProxy(source, new WeakMap(), new WeakMap(), cache);
+ for (const tracked of [first, second]) {
+ expect(
+ Reflect.defineProperty(tracked, 'nested', {value: {value: 2}}),
+ ).toBe(false);
+ expect(Reflect.setPrototypeOf(tracked, null)).toBe(false);
+ expect(Reflect.preventExtensions(tracked)).toBe(false);
+ expect(tracked.nested.value).toBe(1);
+ expect(Reflect.set(tracked.nested, 'value', 2)).toBe(false);
+ expect(Reflect.deleteProperty(tracked.nested, 'value')).toBe(false);
+ }
+ expect(source.nested.value).toBe(1);
+ });
+
+ test('rechecks mutable getter results when their descriptors change', () => {
+ const mutable = {nested: {value: 1}};
+ const source = Object.freeze({
+ get data() {
+ return mutable;
+ },
+ });
+ const cache = new SnapshotTargetCache();
+ const tracked = createProxy(source, new WeakMap(), new WeakMap(), cache);
+ expect(tracked.data.nested.value).toBe(1);
+ expect(cache.has(mutable)).toBe(false);
+ Object.freeze(mutable);
+ expect(tracked.data.nested.value).toBe(1);
+ expect(cache.has(mutable)).toBe(true);
+ expect(
+ Reflect.defineProperty(tracked.data, 'nested', {value: {value: 2}}),
+ ).toBe(false);
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/utils/snapshotTargetCache/snapshotTargetCache.ts b/packages/classy-store/src/frameworks/react/utils/snapshotTargetCache/snapshotTargetCache.ts
new file mode 100644
index 0000000..de0ef4a
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/utils/snapshotTargetCache/snapshotTargetCache.ts
@@ -0,0 +1,31 @@
+const readonlyTarget: ProxyHandler = {
+ set: () => false,
+ deleteProperty: () => false,
+ defineProperty: () => false,
+ setPrototypeOf: () => false,
+ preventExtensions: () => false,
+};
+
+/**
+ * Share proxy-compare's descriptor scans and backing copies of frozen snapshots.
+ * Tracking proxies and read sets remain private to each hook. Weak keys let
+ * unreachable snapshots and their backing copies be collected together.
+ */
+export class SnapshotTargetCache extends WeakMap {
+ override set(key: object, value: unknown): this {
+ // proxy-compare 3's target-cache entries are [original, optional copy].
+ // Getter results can be mutable: never cache their descriptor decisions.
+ const entry = value as [object, object?];
+ if (!Object.isFrozen(entry[0])) return this;
+
+ if (entry[1]) {
+ // Copies need configurable descriptors so nested tracking proxies obey
+ // Proxy invariants. Upstream blocks assignment/deletion, but reflection
+ // could otherwise corrupt this shared copy. Guard the copy, not the
+ // per-consumer proxy. Update the tuple in place: createProxy's first call
+ // continues using this same tuple immediately after cache.set().
+ entry[1] = new Proxy(entry[1], readonlyTarget);
+ }
+ return super.set(key, entry);
+ }
+}
diff --git a/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.qa.test.ts b/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.qa.test.ts
new file mode 100644
index 0000000..16677a3
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.qa.test.ts
@@ -0,0 +1,80 @@
+import {describe, expect, test} from 'bun:test';
+import {createProxy, isChanged} from 'proxy-compare';
+import {TrackingReadMap} from './trackingReadMap';
+
+describe('TrackingReadMap lifecycle QA', () => {
+ test('isolates nested reads from an abandoned render after another render commits', () => {
+ const committed = new WeakMap();
+ const nested = Object.freeze({first: 1, second: 2, third: 3});
+ const source = Object.freeze({nested});
+ const initialReads = new TrackingReadMap(committed);
+ expect(createProxy(source, initialReads).nested.first).toBe(1);
+ initialReads.commit();
+
+ const abandonedReads = new TrackingReadMap(committed);
+ const abandoned = createProxy(source, abandonedReads);
+ expect(abandoned.nested.second).toBe(2);
+ const nextReads = new TrackingReadMap(committed);
+ expect(createProxy(source, nextReads).nested.first).toBe(1);
+ nextReads.commit();
+ // Suspended work may continue reading after a different render committed.
+ expect(abandoned.nested.third).toBe(3);
+
+ const followingReads = new TrackingReadMap(committed);
+ expect(
+ isChanged(
+ source,
+ {nested: {first: 1, second: 20, third: 30}},
+ followingReads,
+ ),
+ ).toBe(false);
+ expect(
+ isChanged(
+ source,
+ {nested: {first: 10, second: 2, third: 3}},
+ followingReads,
+ ),
+ ).toBe(true);
+ });
+
+ test('keeps late reads on committed objects visible to later memoized renders', () => {
+ const committed = new WeakMap();
+ const source = Object.freeze({renderValue: 1, effectValue: 2});
+ const reads = new TrackingReadMap(committed);
+ const state = createProxy(source, reads);
+ expect(state.renderValue).toBe(1);
+ reads.commit();
+ expect(state.effectValue).toBe(2);
+
+ const nextReads = new TrackingReadMap(committed);
+ expect(isChanged(source, {renderValue: 1, effectValue: 3}, nextReads)).toBe(
+ true,
+ );
+ expect(isChanged(source, {renderValue: 2, effectValue: 2}, nextReads)).toBe(
+ true,
+ );
+ expect(isChanged(source, {renderValue: 1, effectValue: 2}, nextReads)).toBe(
+ false,
+ );
+ });
+
+ test('publishes newly read objects on a later commit without reviving deleted entries', () => {
+ const committed = new WeakMap();
+ const first = Object.freeze({value: 1});
+ const second = Object.freeze({value: 2});
+ const discarded = Object.freeze({value: 3});
+ const reads = new TrackingReadMap(committed);
+ expect(createProxy(first, reads).value).toBe(1);
+ reads.commit();
+ expect(createProxy(second, reads).value).toBe(2);
+ expect(createProxy(discarded, reads).value).toBe(3);
+ reads.delete(discarded);
+ reads.commit();
+ reads.commit();
+
+ const nextReads = new TrackingReadMap(committed);
+ expect(isChanged(first, {value: 1, ignored: true}, nextReads)).toBe(false);
+ expect(isChanged(second, {value: 2, ignored: true}, nextReads)).toBe(false);
+ expect(committed.has(discarded)).toBe(false);
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.test.ts b/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.test.ts
new file mode 100644
index 0000000..574eb72
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.test.ts
@@ -0,0 +1,45 @@
+import {describe, expect, test} from 'bun:test';
+import {createProxy, isChanged} from 'proxy-compare';
+import {TrackingReadMap} from './trackingReadMap';
+
+describe('TrackingReadMap', () => {
+ test('retains committed dependencies when a later render reuses a memoized value', () => {
+ const committed = new WeakMap();
+ const source = Object.freeze({first: 1, second: 2});
+ const firstReads = new TrackingReadMap(committed);
+ const first = createProxy(source, firstReads);
+ expect(first.first).toBe(1);
+ firstReads.commit();
+ const nextReads = new TrackingReadMap(committed);
+ const next = createProxy(source, nextReads);
+ expect(next.second).toBe(2);
+ nextReads.commit();
+ expect(isChanged(source, {first: 3, second: 2}, nextReads)).toBe(true);
+ expect(isChanged(source, {first: 1, second: 3}, nextReads)).toBe(true);
+ expect(isChanged(source, {first: 1, second: 2}, nextReads)).toBe(false);
+ });
+
+ test('does not publish reads from an abandoned render', () => {
+ const committed = new WeakMap();
+ const source = Object.freeze({first: 1, second: 2});
+ const originalReads = new TrackingReadMap(committed);
+ expect(createProxy(source, originalReads).first).toBe(1);
+ originalReads.commit();
+ const abandonedReads = new TrackingReadMap(committed);
+ expect(createProxy(source, abandonedReads).second).toBe(2);
+ const nextReads = new TrackingReadMap(committed);
+ expect(isChanged(source, {first: 1, second: 3}, originalReads)).toBe(false);
+ expect(isChanged(source, {first: 1, second: 3}, nextReads)).toBe(false);
+ expect(isChanged(source, {first: 2, second: 2}, nextReads)).toBe(true);
+ });
+
+ test('does not publish a dependency removed before commit', () => {
+ const committed = new WeakMap();
+ const source = Object.freeze({value: 1});
+ const reads = new TrackingReadMap(committed);
+ expect(createProxy(source, reads).value).toBe(1);
+ reads.delete(source);
+ reads.commit();
+ expect(committed.has(source)).toBe(false);
+ });
+});
diff --git a/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.ts b/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.ts
new file mode 100644
index 0000000..47e2c96
--- /dev/null
+++ b/packages/classy-store/src/frameworks/react/utils/trackingReadMap/trackingReadMap.ts
@@ -0,0 +1,52 @@
+/**
+ * A render-local copy of reads associated with immutable snapshot objects.
+ *
+ * Memoized consumers may reuse a value without reading its properties again.
+ * Keep those dependencies while the same snapshot object is reused, including
+ * structurally shared children. Only committed renders publish their reads.
+ */
+export class TrackingReadMap extends WeakMap {
+ private pending: WeakRef[] = [];
+
+ constructor(private readonly committed: WeakMap) {
+ super();
+ }
+
+ override get(target: object): unknown {
+ if (!super.has(target)) {
+ const previous = this.committed.get(target) as
+ | Record
+ | undefined;
+ if (previous) {
+ // proxy-compare 3 stores operation sets in a record. Copy every field
+ // generically rather than depending on its private operation names.
+ // Sharing the sets would let a suspended render change committed reads.
+ const copy = {...previous};
+ for (const key of Reflect.ownKeys(copy)) {
+ const value = copy[key];
+ if (value instanceof Set) copy[key] = new Set(value);
+ }
+ this.set(target, copy);
+ }
+ }
+ return super.get(target);
+ }
+
+ override set(target: object, reads: unknown): this {
+ if (!super.has(target)) this.pending.push(new WeakRef(target));
+ return super.set(target, reads);
+ }
+
+ commit(): void {
+ // A shared child proxy can keep this render's map reachable after newer
+ // snapshots replace its root. Neither the read map nor the commit queue may
+ // strongly retain those roots, including when this render is abandoned.
+ for (const reference of this.pending) {
+ const target = reference.deref();
+ if (target && super.has(target)) {
+ this.committed.set(target, super.get(target));
+ }
+ }
+ this.pending = [];
+ }
+}
diff --git a/packages/classy-store/src/snapshot/snapshot.memory.test.ts b/packages/classy-store/src/snapshot/snapshot.memory.test.ts
new file mode 100644
index 0000000..db8c303
--- /dev/null
+++ b/packages/classy-store/src/snapshot/snapshot.memory.test.ts
@@ -0,0 +1,68 @@
+import {describe, expect, test} from 'bun:test';
+import {createClassyStore} from '../core/core';
+import {snapshot} from './snapshot';
+
+const arrayCases: {
+ description: string;
+ value: {count: number}[];
+ mutate: (value: {count: number}[]) => void;
+ expected: {count: number}[];
+}[] = [
+ {
+ description: 'an array becomes tracked and is truncated',
+ value: [{count: 1}],
+ mutate(value: {count: number}[]) {
+ value.length = 0;
+ },
+ expected: [],
+ },
+ {
+ description: 'an array becomes tracked and its child is replaced',
+ value: [{count: 1}],
+ mutate(value: {count: number}[]) {
+ value[0] = {count: 2};
+ },
+ expected: [{count: 2}],
+ },
+];
+
+describe('snapshot cache ownership', () => {
+ test.each(
+ arrayCases,
+ )('releases the obsolete untracked clone when $description', ({
+ value,
+ mutate,
+ expected,
+ }) => {
+ const store = createClassyStore({value, stable: {label: 'unchanged'}});
+ const initial = snapshot(store);
+
+ mutate(store.value);
+ const current = snapshot(store);
+
+ // A new owner must not receive the obsolete deep-clone cache entry that
+ // belonged to this array before it acquired a tracked/versioned snapshot.
+ const newOwner = createClassyStore({value});
+ expect(snapshot(newOwner).value).toEqual(expected);
+ expect(current.value).toEqual(expected);
+ expect(current.stable).toBe(initial.stable);
+ expect(snapshot(store)).toBe(current);
+ expect(initial.value).toEqual([{count: 1}]);
+ expect(Object.isFrozen(current.value)).toBe(true);
+ });
+
+ test('releases the obsolete untracked clone when a plain object becomes tracked', () => {
+ const value = {nested: {count: 1}, unchanged: {label: 'shared'}};
+ const store = createClassyStore({value});
+ const initial = snapshot(store);
+
+ store.value.nested = {count: 2};
+ const current = snapshot(store);
+ const newOwner = createClassyStore({value});
+
+ expect(snapshot(newOwner).value.nested.count).toBe(2);
+ expect(current.value.unchanged).toBe(initial.value.unchanged);
+ expect(initial.value.nested.count).toBe(1);
+ expect(snapshot(store)).toBe(current);
+ });
+});
diff --git a/packages/classy-store/src/snapshot/snapshot.test.ts b/packages/classy-store/src/snapshot/snapshot.test.ts
index ca7ec3e..44472c6 100644
--- a/packages/classy-store/src/snapshot/snapshot.test.ts
+++ b/packages/classy-store/src/snapshot/snapshot.test.ts
@@ -1,11 +1,89 @@
import {describe, expect, it} from 'bun:test';
+import {createProxy, isChanged} from 'proxy-compare';
import {createClassyStore} from '../core/core';
+import {PROXYABLE} from '../utils/internal/internal';
import {snapshot} from './snapshot';
/** Helper: flush the queueMicrotask-based batching. */
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
describe('snapshot()', () => {
+ describe('property read tracking', () => {
+ it('tracks class-root properties without subscribing to unrelated fields', () => {
+ class Store {
+ count = 0;
+ other = 0;
+ }
+ const store = createClassyStore(new Store());
+ const previous = snapshot(store);
+ const affected = new WeakMap();
+ const tracked = createProxy(previous, affected);
+ expect(tracked.count).toBe(0);
+
+ store.other++;
+ expect(isChanged(previous, snapshot(store), affected)).toBe(false);
+ store.count++;
+ expect(isChanged(previous, snapshot(store), affected)).toBe(true);
+ });
+
+ it.each([
+ {description: 'previously accessed', accessBeforeSnapshot: true},
+ {description: 'previously unaccessed', accessBeforeSnapshot: false},
+ ])('tracks $description nested opted-in classes', ({
+ accessBeforeSnapshot,
+ }) => {
+ class Child {
+ static [PROXYABLE] = true;
+ count = 0;
+ other = 0;
+ }
+ const store = createClassyStore({child: new Child()});
+ if (accessBeforeSnapshot) expect(store.child.count).toBe(0);
+ const previous = snapshot(store);
+ const affected = new WeakMap();
+ const tracked = createProxy(previous, affected);
+ expect(tracked.child.count).toBe(0);
+
+ store.child.other++;
+ expect(isChanged(previous, snapshot(store), affected)).toBe(false);
+ store.child.count++;
+ expect(isChanged(previous, snapshot(store), affected)).toBe(true);
+ });
+
+ it('tracks null-prototype store properties', () => {
+ const store = createClassyStore(
+ Object.assign(Object.create(null), {count: 0, other: 0}) as {
+ count: number;
+ other: number;
+ },
+ );
+ const previous = snapshot(store);
+ const affected = new WeakMap();
+ expect(createProxy(previous, affected).count).toBe(0);
+ store.other++;
+ expect(isChanged(previous, snapshot(store), affected)).toBe(false);
+ store.count++;
+ expect(isChanged(previous, snapshot(store), affected)).toBe(true);
+ });
+
+ it('leaves frozen opaque values unwrapped and usable', () => {
+ class OpaqueValue {
+ value = 3;
+ }
+ const date = Object.freeze(new Date('2026-01-01'));
+ const map = Object.freeze(new Map([['count', 2]]));
+ const opaque = Object.freeze(new OpaqueValue());
+ const store = createClassyStore({date, map, opaque});
+ const tracked = createProxy(snapshot(store), new WeakMap());
+
+ expect(tracked.date).toBe(date);
+ expect(tracked.date.getTime()).toBe(date.getTime());
+ expect(tracked.map).toBe(map);
+ expect(tracked.map.get('count')).toBe(2);
+ expect(tracked.opaque).toBe(opaque);
+ });
+ });
+
// ββ Freezing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
describe('freezing', () => {
diff --git a/packages/classy-store/src/snapshot/snapshot.ts b/packages/classy-store/src/snapshot/snapshot.ts
index 80d1e65..2f906ac 100644
--- a/packages/classy-store/src/snapshot/snapshot.ts
+++ b/packages/classy-store/src/snapshot/snapshot.ts
@@ -1,3 +1,4 @@
+import {markToTrack} from 'proxy-compare';
import {getInternal} from '../core/core';
import type {Snapshot, StoreInternal} from '../types';
import {canProxy, findGetterDescriptor} from '../utils/internal/internal';
@@ -26,6 +27,14 @@ const snapshotGetterCache = new WeakMap<
Map
>();
+// Restoration must distinguish generated memoization getters from user accessors.
+const memoizedSnapshotGetters = new WeakSet<() => unknown>();
+
+/** Internal helper for restoring live prototype getters from a snapshot. */
+export function isMemoizedSnapshotGetter(getter: (() => unknown) | undefined) {
+ return getter !== undefined && memoizedSnapshotGetters.has(getter);
+}
+
// ββ Cross-snapshot getter memoization βββββββββββββββββββββββββββββββββββββββββ
/**
@@ -157,6 +166,22 @@ function evaluateSnapshotGetter(
// ββ Internal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function markSnapshotForTracking(value: object): void {
+ // proxy-compare 3 copies frozen string descriptors, but leaves symbol
+ // descriptors frozen. Wrapping an object-valued symbol then violates Proxy
+ // invariants. Compare those nodes as a whole until that dependency is fixed.
+ const hasObjectSymbol = Object.getOwnPropertySymbols(value).some((key) => {
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
+ return (
+ descriptor &&
+ 'value' in descriptor &&
+ typeof descriptor.value === 'object' &&
+ descriptor.value !== null
+ );
+ });
+ markToTrack(value, !hasObjectSymbol);
+}
+
/**
* Resolve a single property value into its snapshot equivalent.
*
@@ -220,6 +245,9 @@ function deepFreezeClone(value: object, inProgress: WeakSet): object {
}
}
+ // Class and null-prototype snapshots need an explicit opt-in for read tracking.
+ // Only cloned state is marked; opaque leaves retain their existing behavior.
+ markSnapshotForTracking(clone);
Object.freeze(clone);
untrackedCache.set(value, clone);
inProgress.delete(value);
@@ -271,10 +299,12 @@ function installMemoizedGetters(
): void {
const getters = collectGetters(target);
for (const [key, getterFn] of getters) {
+ function memoizedGetter(this: object) {
+ return evaluateSnapshotGetter(this, target, key, getterFn);
+ }
+ memoizedSnapshotGetters.add(memoizedGetter);
Object.defineProperty(snap, key, {
- get() {
- return evaluateSnapshotGetter(this as object, target, key, getterFn);
- },
+ get: memoizedGetter,
enumerable: true,
configurable: true, // required so Object.freeze can make it non-configurable
});
@@ -298,6 +328,9 @@ function createSnapshotRecursive(
internal: StoreInternal,
inProgress: WeakSet = new WeakSet(),
): T {
+ // Lazy proxy creation promotes previously untracked data to versioned caching.
+ // Its obsolete deep clone must not retain removed data for the live target.
+ untrackedCache.delete(target);
// Cache hit: version unchanged β return the same frozen snapshot reference.
const cached = snapshotCache.get(target);
if (cached && cached[0] === internal.version) {
@@ -345,6 +378,7 @@ function createSnapshotRecursive(
installMemoizedGetters(snap as Record, target);
}
+ markSnapshotForTracking(snap);
Object.freeze(snap);
// Cache AFTER populating + freezing. The reference is stable.
snapshotCache.set(target, [internal.version, snap]);
diff --git a/packages/classy-store/src/types.snapshot.test.ts b/packages/classy-store/src/types.snapshot.test.ts
new file mode 100644
index 0000000..d59363c
--- /dev/null
+++ b/packages/classy-store/src/types.snapshot.test.ts
@@ -0,0 +1,107 @@
+import {describe, expect, expectTypeOf, test} from 'bun:test';
+import {reactiveMap, reactiveSet} from './collections/collections';
+import {createClassyStore} from './core/core';
+import {snapshot} from './snapshot/snapshot';
+import type {Snapshot} from './types';
+
+function formatAmount(amount: number, prefix = '$'): string {
+ return `${prefix}${amount}`;
+}
+
+function identity(value: Value): Value {
+ return value;
+}
+
+function convert(value: number): string;
+function convert(value: string): number;
+function convert(value: string | number): string | number {
+ return typeof value === 'number' ? String(value) : Number(value);
+}
+
+function joinValues(prefix: string, ...values: number[]): string {
+ return `${prefix}${values.join(',')}`;
+}
+
+describe('Snapshot callable types', () => {
+ test('preserves typed parameters, optional parameters, and rest parameters', () => {
+ const state = snapshot(createClassyStore({formatAmount, joinValues}));
+
+ expectTypeOf(state.formatAmount).toEqualTypeOf();
+ expectTypeOf(state.joinValues).toEqualTypeOf();
+ expectTypeOf>().toEqualTypeOf<
+ typeof formatAmount
+ >();
+ expectTypeOf(state.formatAmount(3)).toEqualTypeOf();
+ expect(state.formatAmount(3)).toBe('$3');
+ expect(state.formatAmount(4, 'EUR ')).toBe('EUR 4');
+ expect(state.joinValues('values: ', 1, 2, 3)).toBe('values: 1,2,3');
+ });
+
+ test('preserves generic inference and every overload', () => {
+ const state = snapshot(createClassyStore({identity, convert}));
+
+ expectTypeOf(state.identity).toEqualTypeOf();
+ expectTypeOf(state.convert).toEqualTypeOf();
+ expectTypeOf(state.identity('literal' as const)).toEqualTypeOf<'literal'>();
+ expectTypeOf(state.identity({id: 1})).toEqualTypeOf<{id: number}>();
+ expectTypeOf(state.convert(12)).toEqualTypeOf();
+ expectTypeOf(state.convert('12')).toEqualTypeOf();
+ expect(state.identity('literal')).toBe('literal');
+ expect(state.convert(12)).toBe('12');
+ expect(state.convert('12')).toBe(12);
+ });
+
+ test('keeps typed reactive collection queries callable', () => {
+ const store = createClassyStore({
+ values: reactiveMap([
+ ['first', {label: 'initial'}],
+ ]),
+ tags: reactiveSet(['first']),
+ });
+ const initial = snapshot(store);
+
+ expectTypeOf(initial.values.get).toEqualTypeOf();
+ expectTypeOf(initial.tags.has).toEqualTypeOf();
+ expect(initial.values.get('first')).toEqual({label: 'initial'});
+ expect(initial.tags.has('first')).toBe(true);
+
+ store.values.set('second', {label: 'updated'});
+ store.tags.add('second');
+ expect(initial.values.get('second')).toBeUndefined();
+ expect(initial.tags.has('second')).toBe(false);
+ expect(snapshot(store).values.get('second')).toEqual({label: 'updated'});
+ expect(snapshot(store).tags.has('second')).toBe(true);
+ });
+
+ test('still rejects assignment to readonly data, nested values, and arrays', () => {
+ const state = snapshot(
+ createClassyStore({
+ title: 'initial',
+ nested: {label: 'initial'},
+ rows: [{label: 'initial'}],
+ formatAmount,
+ }),
+ );
+
+ expect(() => {
+ // @ts-expect-error Snapshot properties remain readonly.
+ state.title = 'changed';
+ }).toThrow();
+ expect(() => {
+ // @ts-expect-error Nested snapshot properties remain readonly.
+ state.nested.label = 'changed';
+ }).toThrow();
+ expect(() => {
+ // @ts-expect-error Snapshot arrays do not expose mutable push.
+ state.rows.push({label: 'changed'});
+ }).toThrow();
+ expect(() => {
+ // @ts-expect-error Snapshot array elements remain deeply readonly.
+ state.rows[0].label = 'changed';
+ }).toThrow();
+ expect(() => {
+ // @ts-expect-error Callable properties themselves remain readonly.
+ state.formatAmount = () => 'changed';
+ }).toThrow();
+ });
+});
diff --git a/packages/classy-store/src/types.ts b/packages/classy-store/src/types.ts
index 02ebe5f..ba6c439 100644
--- a/packages/classy-store/src/types.ts
+++ b/packages/classy-store/src/types.ts
@@ -9,7 +9,9 @@ type Primitive = string | number | boolean | null | undefined | symbol | bigint;
* Functions are treated as snapshot leaves -- they pass through unchanged
* because freezing or cloning them would break callable references.
*/
-type AnyFunction = (...args: unknown[]) => unknown;
+// This classifies functions without invoking them. `unknown[]` would reject
+// typed parameters under strictFunctionTypes and map their signatures to {}.
+type AnyFunction = (...args: never[]) => unknown;
/**
* Types that `snapshot()` returns as-is (no deep clone or freeze).
diff --git a/packages/classy-store/src/utils/cloneSnapshot/cloneSnapshot.test.ts b/packages/classy-store/src/utils/cloneSnapshot/cloneSnapshot.test.ts
new file mode 100644
index 0000000..d07f6a3
--- /dev/null
+++ b/packages/classy-store/src/utils/cloneSnapshot/cloneSnapshot.test.ts
@@ -0,0 +1,140 @@
+import {describe, expect, it} from 'bun:test';
+import {createClassyStore} from '../../core/core';
+import {snapshot} from '../../snapshot/snapshot';
+import {PROXYABLE} from '../internal/internal';
+import {cloneSnapshotValue} from './cloneSnapshot';
+
+describe('cloneSnapshotValue', () => {
+ it('copies frozen graphs without losing aliases, symbols, or opaque leaves', () => {
+ const symbol = Symbol('metadata');
+ const date = new Date('2026-01-01');
+ const map = new Map([['key', 'value']]);
+ const method = () => 'value';
+ const shared = Object.freeze({count: 1});
+ const source = Object.create(null);
+ Object.defineProperties(source, {
+ first: {value: shared, enumerable: true},
+ second: {value: shared, enumerable: false},
+ date: {value: date, enumerable: true},
+ map: {value: map, enumerable: true},
+ method: {value: method, enumerable: true},
+ [symbol]: {value: shared, enumerable: true},
+ });
+ Object.freeze(source);
+
+ const clone = cloneSnapshotValue(source, new WeakMap()) as typeof source;
+
+ expect(Object.getPrototypeOf(clone)).toBeNull();
+ expect(clone.first).not.toBe(shared);
+ expect(clone.first).toBe(clone.second);
+ expect(clone[symbol]).toBe(clone.first);
+ expect(clone.date).toBe(date);
+ expect(clone.map).toBe(map);
+ expect(clone.method).toBe(method);
+ expect(Object.getOwnPropertyDescriptor(clone, 'second')).toMatchObject({
+ enumerable: false,
+ configurable: true,
+ writable: true,
+ });
+ clone.first.count = 2;
+ expect(shared.count).toBe(1);
+ });
+
+ it('retains cycles without linking the copy back to the source', () => {
+ const source: {count: number; self?: unknown} = {count: 1};
+ source.self = source;
+ Object.freeze(source);
+
+ const clone = cloneSnapshotValue(source, new WeakMap()) as typeof source;
+
+ expect(clone).not.toBe(source);
+ expect(clone.self).toBe(clone);
+ clone.count = 2;
+ expect(source.count).toBe(1);
+ });
+
+ it('preserves sparse array holes, trailing length, and custom properties', () => {
+ const symbol = Symbol('metadata');
+ const source = new Array<{count: number}>(5);
+ source[2] = Object.freeze({count: 1});
+ Object.defineProperty(source, symbol, {value: source[2]});
+ Object.freeze(source);
+
+ const clone = cloneSnapshotValue(source, new WeakMap()) as typeof source;
+
+ expect(clone).toHaveLength(5);
+ expect(0 in clone).toBe(false);
+ expect(1 in clone).toBe(false);
+ expect(2 in clone).toBe(true);
+ expect(4 in clone).toBe(false);
+ expect(Reflect.get(clone, symbol)).toBe(clone[2]);
+ clone[2].count = 2;
+ clone.push({count: 3});
+ expect(source[2].count).toBe(1);
+ expect(source).toHaveLength(5);
+ });
+
+ it('restores live prototype getters from memoized snapshots', () => {
+ class Profile {
+ static [PROXYABLE] = true;
+ name = 'Alice';
+
+ get greeting() {
+ return `Hello, ${this.name}`;
+ }
+ }
+ const store = createClassyStore(new Profile());
+ const previous = snapshot(store);
+ expect(previous.greeting).toBe('Hello, Alice');
+
+ const clone = cloneSnapshotValue(previous, new WeakMap()) as Profile;
+ clone.name = 'Alicia';
+
+ expect(clone).toBeInstanceOf(Profile);
+ expect(clone.greeting).toBe('Hello, Alicia');
+ expect(previous.greeting).toBe('Hello, Alice');
+ });
+
+ it('preserves frozen custom accessors that override prototype getters', () => {
+ class Profile {
+ static [PROXYABLE] = true;
+ name = 'Alice';
+
+ get greeting() {
+ return `Hello, ${this.name}`;
+ }
+ }
+ const source = new Profile();
+ Object.defineProperty(source, 'greeting', {
+ get(this: Profile) {
+ return `Welcome, ${this.name}`;
+ },
+ enumerable: true,
+ configurable: true,
+ });
+ Object.freeze(source);
+
+ const clone = cloneSnapshotValue(source, new WeakMap()) as Profile;
+ clone.name = 'Alicia';
+
+ expect(clone.greeting).toBe('Welcome, Alicia');
+ expect(source.greeting).toBe('Welcome, Alice');
+ });
+
+ it('preserves the prototype of custom array values', () => {
+ class Names extends Array {
+ get firstName() {
+ return this[0];
+ }
+ }
+ const source = new Names('Alice');
+ Object.freeze(source);
+
+ const clone = cloneSnapshotValue(source, new WeakMap()) as Names;
+
+ expect(clone).toBeInstanceOf(Names);
+ clone[0] = 'Alicia';
+ expect(clone.firstName).toBe('Alicia');
+ expect(source.firstName).toBe('Alice');
+ });
+});
diff --git a/packages/classy-store/src/utils/cloneSnapshot/cloneSnapshot.ts b/packages/classy-store/src/utils/cloneSnapshot/cloneSnapshot.ts
new file mode 100644
index 0000000..dc268d4
--- /dev/null
+++ b/packages/classy-store/src/utils/cloneSnapshot/cloneSnapshot.ts
@@ -0,0 +1,44 @@
+import {isMemoizedSnapshotGetter} from '../../snapshot/snapshot';
+import {canProxy} from '../internal/internal';
+
+/** Restore proxyable snapshot data as mutable objects, preserving prototypes. */
+export function cloneSnapshotValue(
+ value: unknown,
+ clones: WeakMap,
+): unknown {
+ // Match snapshot leaf semantics: opaque values and functions pass through.
+ if (!canProxy(value)) return value;
+ const cached = clones.get(value);
+ if (cached) return cached;
+
+ const clone: object = Array.isArray(value)
+ ? Object.setPrototypeOf(
+ new Array(value.length),
+ Object.getPrototypeOf(value),
+ )
+ : Object.create(Object.getPrototypeOf(value));
+ clones.set(value, clone);
+
+ for (const key of Reflect.ownKeys(value)) {
+ if (Array.isArray(value) && key === 'length') continue;
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
+ if (!descriptor) continue;
+ if (!('value' in descriptor)) {
+ // Snapshot getters cache frozen values; use the original prototype getters.
+ if (isMemoizedSnapshotGetter(descriptor.get)) {
+ continue;
+ }
+ // Custom persistence merges and transforms can supply their own accessors.
+ Object.defineProperty(clone, key, {...descriptor, configurable: true});
+ continue;
+ }
+ Object.defineProperty(clone, key, {
+ value: cloneSnapshotValue(descriptor.value, clones),
+ enumerable: descriptor.enumerable,
+ configurable: true,
+ writable: true,
+ });
+ }
+
+ return clone;
+}
diff --git a/packages/classy-store/src/utils/history/history.test.ts b/packages/classy-store/src/utils/history/history.test.ts
index c47aee6..2931f07 100644
--- a/packages/classy-store/src/utils/history/history.test.ts
+++ b/packages/classy-store/src/utils/history/history.test.ts
@@ -1,5 +1,7 @@
import {describe, expect, it} from 'bun:test';
+import {reactiveMap, reactiveSet} from '../../collections/collections';
import {createClassyStore} from '../../core/core';
+import {snapshot} from '../../snapshot/snapshot';
import {withHistory} from './history';
/** Flush the queueMicrotask-based batching. */
@@ -8,6 +10,103 @@ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
// ββ Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
describe('withHistory', () => {
+ it.each([
+ {description: 'undo', operation: 'undo' as const},
+ {description: 'redo', operation: 'redo' as const},
+ ])('keeps nested state editable after $description without changing history', async ({
+ operation,
+ }) => {
+ const store = createClassyStore({
+ items: [{name: 'initial'}],
+ details: {nested: {count: 0}},
+ });
+ const history = withHistory(store);
+
+ try {
+ store.items[0].name = 'recorded';
+ store.details.nested.count = 1;
+ await tick();
+
+ history.undo();
+ await tick();
+ if (operation === 'redo') {
+ history.redo();
+ await tick();
+ }
+ const restored = snapshot(store);
+ const restoredName = operation === 'undo' ? 'initial' : 'recorded';
+ const restoredCount = operation === 'undo' ? 0 : 1;
+
+ store.items.push({name: 'added'});
+ store.items[0].name = 'edited';
+ store.details.nested.count = 2;
+ await tick();
+
+ expect(snapshot(store).items).toEqual([
+ {name: 'edited'},
+ {name: 'added'},
+ ]);
+ expect(store.details.nested.count).toBe(2);
+ expect(restored.items).toEqual([{name: restoredName}]);
+ expect(restored.details.nested.count).toBe(restoredCount);
+ expect(history.canRedo).toBe(false);
+
+ history.undo();
+ await tick();
+ expect(snapshot(store).items).toEqual([{name: restoredName}]);
+ expect(store.details.nested.count).toBe(restoredCount);
+
+ history.redo();
+ await tick();
+ expect(snapshot(store).items).toEqual([
+ {name: 'edited'},
+ {name: 'added'},
+ ]);
+ expect(store.details.nested.count).toBe(2);
+ } finally {
+ history.dispose();
+ }
+ });
+
+ it('preserves reactive collection methods and getters after restoration', async () => {
+ const date = new Date('2026-01-01');
+ const store = createClassyStore({
+ users: reactiveMap([['alice', {name: 'Alice', date}]]),
+ tags: reactiveSet(['initial']),
+ });
+ const history = withHistory(store);
+
+ try {
+ store.users.set('bob', {name: 'Bob', date});
+ store.tags.add('recorded');
+ await tick();
+
+ history.undo();
+ await tick();
+
+ store.users.set('carol', {name: 'Carol', date});
+ store.tags.add('edited');
+ const alice = store.users.get('alice');
+ if (!alice) throw new Error('Expected the restored user');
+ alice.name = 'Alicia';
+ await tick();
+
+ expect(store.users.size).toBe(2);
+ expect(store.tags.size).toBe(2);
+ expect(store.users.get('alice')?.name).toBe('Alicia');
+ expect(store.users.get('alice')?.date).toBe(date);
+ expect(Array.from(store.tags)).toEqual(['initial', 'edited']);
+
+ history.undo();
+ await tick();
+ expect(store.users.size).toBe(1);
+ expect(store.users.get('alice')?.name).toBe('Alice');
+ expect(Array.from(store.tags)).toEqual(['initial']);
+ } finally {
+ history.dispose();
+ }
+ });
+
it('captures initial state as history[0]', () => {
class Store {
count = 0;
diff --git a/packages/classy-store/src/utils/history/history.ts b/packages/classy-store/src/utils/history/history.ts
index ba998e6..478058b 100644
--- a/packages/classy-store/src/utils/history/history.ts
+++ b/packages/classy-store/src/utils/history/history.ts
@@ -1,6 +1,7 @@
import {subscribe} from '../../core/core';
import {snapshot} from '../../snapshot/snapshot';
import type {Snapshot} from '../../types';
+import {cloneSnapshotValue} from '../cloneSnapshot/cloneSnapshot';
import {findGetterDescriptor} from '../internal/internal';
// ββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -57,6 +58,7 @@ export function withHistory(
*/
function applySnapshot(snap: Snapshot): void {
const snapRecord = snap as Record;
+ const clones = new WeakMap();
for (const key of Object.keys(snapRecord)) {
// Skip getters
@@ -65,7 +67,10 @@ export function withHistory(
if (typeof (proxyStore as Record)[key] === 'function') {
continue;
}
- (proxyStore as Record)[key] = snapRecord[key];
+ (proxyStore as Record)[key] = cloneSnapshotValue(
+ snapRecord[key],
+ clones,
+ );
}
}
diff --git a/packages/classy-store/src/utils/persist/persist.concurrent.test.ts b/packages/classy-store/src/utils/persist/persist.concurrent.test.ts
new file mode 100644
index 0000000..7771b0d
--- /dev/null
+++ b/packages/classy-store/src/utils/persist/persist.concurrent.test.ts
@@ -0,0 +1,242 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {createClassyStore} from '../../core/core';
+import {type PersistHandle, persist} from './persist';
+
+function deferred() {
+ let resolve!: (value: T) => void;
+ let reject!: (error: Error) => void;
+ const promise = new Promise((resolvePromise, rejectPromise) => {
+ resolve = resolvePromise;
+ reject = rejectPromise;
+ });
+ return {promise, resolve, reject};
+}
+
+const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
+const envelope = (count: number) =>
+ JSON.stringify({version: 0, state: {count}});
+const handles: PersistHandle[] = [];
+afterEach(() => {
+ for (const handle of handles.splice(0)) handle.unsubscribe();
+});
+
+describe('persist async operation ordering', () => {
+ test.each([
+ {description: 'successful', fails: false},
+ {description: 'failed', fails: true},
+ ])('keeps a newer save behind a $description pending write', async ({
+ fails,
+ }) => {
+ const firstWrite = deferred();
+ const secondWrite = deferred();
+ let saved: string | null = null;
+ const storage = {
+ getItem: () => saved,
+ setItem: mock(async (_name: string, value: string) => {
+ await (JSON.parse(value).state.count === 1 ? firstWrite : secondWrite)
+ .promise;
+ saved = value;
+ }),
+ removeItem: () => {},
+ };
+ const onError = mock(() => {});
+ const store = createClassyStore({count: 1});
+ const handle = persist(store, {
+ name: 'draft',
+ storage,
+ skipHydration: true,
+ debounce: 1000,
+ onError,
+ });
+ handles.push(handle);
+ const first = handle.save();
+ store.count = 2;
+ await tick();
+ const second = handle.save();
+ store.count = 3; // The queued save must retain its requested value, 2.
+
+ try {
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+ if (fails) firstWrite.reject(new Error('Write failed'));
+ else firstWrite.resolve();
+ await first;
+ await tick();
+ expect(storage.setItem).toHaveBeenCalledTimes(2);
+ secondWrite.resolve();
+ await second;
+ expect(saved).toBe(envelope(2));
+ expect(onError).toHaveBeenCalledTimes(fails ? 1 : 0);
+ } finally {
+ firstWrite.resolve();
+ secondWrite.resolve();
+ await Promise.all([first, second]);
+ }
+ });
+
+ test('drops queued saves after unsubscribe while an issued write can finish', async () => {
+ const pending = deferred();
+ const storage = {
+ getItem: () => null,
+ setItem: mock(() => pending.promise),
+ removeItem: () => {},
+ };
+ const store = createClassyStore({count: 1});
+ const handle = persist(store, {
+ name: 'draft',
+ storage,
+ skipHydration: true,
+ });
+ handles.push(handle);
+ const first = handle.save();
+ const second = handle.save();
+ handle.unsubscribe();
+ pending.resolve();
+ await Promise.all([first, second]);
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+ });
+
+ test('clears storage after an already-issued save finishes', async () => {
+ const pending = deferred();
+ let saved: string | null = null;
+ const storage = {
+ getItem: () => saved,
+ setItem: async (_name: string, value: string) => {
+ await pending.promise;
+ saved = value;
+ },
+ removeItem: mock(() => {
+ saved = null;
+ }),
+ };
+ const store = createClassyStore({count: 1});
+ const handle = persist(store, {
+ name: 'draft',
+ storage,
+ skipHydration: true,
+ });
+ handles.push(handle);
+ const saving = handle.save();
+ const clearing = handle.clear();
+ try {
+ expect(storage.removeItem).not.toHaveBeenCalled();
+ } finally {
+ pending.resolve();
+ await Promise.all([saving, clearing]);
+ }
+ expect(saved).toBeNull();
+ expect(storage.removeItem).toHaveBeenCalledTimes(1);
+ });
+
+ test('ignores an older hydration result that finishes after a newer request', async () => {
+ const older = deferred();
+ const newer = deferred();
+ const storage = {
+ getItem: mock()
+ .mockReturnValueOnce(older.promise)
+ .mockReturnValueOnce(newer.promise),
+ setItem: mock(() => {}),
+ removeItem: () => {},
+ };
+ const store = createClassyStore({count: 0});
+ const handle = persist(store, {
+ name: 'draft',
+ storage,
+ skipHydration: true,
+ });
+ handles.push(handle);
+ const first = handle.rehydrate();
+ const second = handle.rehydrate();
+ newer.resolve(envelope(2));
+ await second;
+ older.resolve(envelope(1));
+ await first;
+ expect(store.count).toBe(2);
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ test('keeps edits made while initial hydration is reading storage', async () => {
+ const pendingRead = deferred();
+ let saved: string | null = null;
+ const storage = {
+ getItem: () => pendingRead.promise,
+ setItem: (_name: string, value: string) => {
+ saved = value;
+ },
+ removeItem: () => {},
+ };
+ const store = createClassyStore({count: 0});
+ const handle = persist(store, {name: 'draft', storage, debounce: 1000});
+ handles.push(handle);
+ store.count = 2;
+ await tick();
+ pendingRead.resolve(envelope(1));
+ await handle.hydrated;
+ expect(store.count).toBe(2);
+ await handle.save();
+ expect(saved).toBe(envelope(2));
+ });
+
+ test.each([
+ {description: 'batched', debounce: 0},
+ {description: 'debounced', debounce: 20},
+ ])('clear cancels a pending $description save and later edits still persist', async ({
+ debounce,
+ }) => {
+ let saved: string | null = envelope(1);
+ const storage = {
+ getItem: () => saved,
+ setItem: mock((_name: string, value: string) => {
+ saved = value;
+ }),
+ removeItem: () => {
+ saved = null;
+ },
+ };
+ const store = createClassyStore({count: 1});
+ const handle = persist(store, {
+ name: 'draft',
+ storage,
+ debounce,
+ skipHydration: true,
+ });
+ handles.push(handle);
+ store.count = 2;
+ if (debounce) await tick(); // Arm the timer; the zero-delay case still has a batched notification.
+ await handle.clear();
+ await new Promise((resolve) => setTimeout(resolve, debounce + 10));
+ expect(saved).toBeNull();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ store.count = 3;
+ await new Promise((resolve) => setTimeout(resolve, debounce + 10));
+ expect(saved).toBe(envelope(3));
+ });
+
+ test('rehydrates from the result of a previously requested save', async () => {
+ const pendingWrite = deferred();
+ let saved = envelope(0);
+ const storage = {
+ getItem: mock(() => saved),
+ setItem: async (_name: string, value: string) => {
+ await pendingWrite.promise;
+ saved = value;
+ },
+ removeItem: () => {},
+ };
+ const store = createClassyStore({count: 1});
+ const handle = persist(store, {
+ name: 'draft',
+ storage,
+ skipHydration: true,
+ });
+ handles.push(handle);
+ const saving = handle.save();
+ const reading = handle.rehydrate();
+ try {
+ expect(storage.getItem).not.toHaveBeenCalled();
+ } finally {
+ pendingWrite.resolve();
+ await Promise.all([saving, reading]);
+ }
+ expect(store.count).toBe(1);
+ });
+});
diff --git a/packages/classy-store/src/utils/persist/persist.test.ts b/packages/classy-store/src/utils/persist/persist.test.ts
index b26a461..b416f1e 100644
--- a/packages/classy-store/src/utils/persist/persist.test.ts
+++ b/packages/classy-store/src/utils/persist/persist.test.ts
@@ -1,7 +1,9 @@
import {describe, expect, it, mock} from 'bun:test';
import type {ReactiveMap} from '../../collections/collections';
-import {reactiveMap} from '../../collections/collections';
+import {reactiveMap, reactiveSet} from '../../collections/collections';
import {createClassyStore} from '../../core/core';
+import {snapshot} from '../../snapshot/snapshot';
+import {PROXYABLE} from '../internal/internal';
import type {StorageAdapter} from './persist';
import {persist} from './persist';
@@ -413,6 +415,166 @@ describe('persist()', () => {
// ββ Merge strategies ββββββββββββββββββββββββββββββββββββββββββββββββββββ
describe('merge strategies', () => {
+ it.each([
+ {description: 'shallow initial hydration', manual: false, custom: false},
+ {description: 'shallow rehydration', manual: true, custom: false},
+ {description: 'custom initial hydration', manual: false, custom: true},
+ {description: 'custom rehydration', manual: true, custom: true},
+ ])('keeps omitted nested defaults editable after $description', async ({
+ manual,
+ custom,
+ }) => {
+ const storage = createMockStorage();
+ storage.data.set(
+ 'test',
+ JSON.stringify({version: 0, state: {theme: 'dark'}}),
+ );
+ const setItem = mock(storage.setItem.bind(storage));
+ storage.setItem = setItem;
+ const store = createClassyStore({
+ theme: 'light',
+ items: [{name: 'initial'}],
+ preferences: {display: {compact: false}},
+ });
+ const beforeHydration = snapshot(store);
+ const handle = persist(store, {
+ name: 'test',
+ storage,
+ skipHydration: manual,
+ merge: custom
+ ? (persisted, current) => ({
+ theme: persisted.theme,
+ items: [...(current.items as unknown[])],
+ preferences: {...(current.preferences as object)},
+ })
+ : 'shallow',
+ });
+
+ try {
+ if (manual) await handle.rehydrate();
+ await handle.hydrated;
+ const restored = snapshot(store);
+
+ store.items.push({name: 'added'});
+ store.items[0].name = 'edited';
+ store.preferences.display.compact = true;
+
+ expect(setItem).not.toHaveBeenCalled();
+ await tick();
+ expect(setItem).toHaveBeenCalledTimes(1);
+ expect(parseStored(storage, 'test')?.state).toEqual({
+ theme: 'dark',
+ items: [{name: 'edited'}, {name: 'added'}],
+ preferences: {display: {compact: true}},
+ });
+ expect(beforeHydration.theme).toBe('light');
+ expect(restored.theme).toBe('dark');
+ for (const previous of [beforeHydration, restored]) {
+ expect(previous.items).toEqual([{name: 'initial'}]);
+ expect(previous.preferences.display.compact).toBe(false);
+ }
+ } finally {
+ handle.unsubscribe();
+ }
+ });
+
+ it('preserves reactive defaults and opaque leaves during partial hydration', async () => {
+ class Profile {
+ static [PROXYABLE] = true;
+ name = 'Alice';
+
+ get greeting() {
+ return `Hello, ${this.name}`;
+ }
+
+ rename(name: string) {
+ this.name = name;
+ }
+ }
+
+ const storage = createMockStorage();
+ storage.data.set(
+ 'test',
+ JSON.stringify({version: 0, state: {theme: 'dark'}}),
+ );
+ const date = new Date('2026-01-01');
+ const opaque = new Map([['original', 1]]);
+ const store = createClassyStore({
+ theme: 'light',
+ profile: new Profile(),
+ users: reactiveMap([['alice', {name: 'Alice', date}]]),
+ tags: reactiveSet(['initial']),
+ opaque,
+ });
+ const beforeHydration = snapshot(store);
+ const handle = persist(store, {name: 'test', storage});
+
+ try {
+ await handle.hydrated;
+ expect(store.profile).toBeInstanceOf(Profile);
+ expect(store.profile.greeting).toBe('Hello, Alice');
+ expect(store.users.size).toBe(1);
+ expect(store.tags.size).toBe(1);
+
+ store.profile.rename('Alicia');
+ store.users.set('bob', {name: 'Bob', date});
+ store.tags.add('added');
+ const alice = store.users.get('alice');
+ if (!alice) throw new Error('Expected the restored user');
+ alice.name = 'Alicia';
+
+ expect(store.profile.greeting).toBe('Hello, Alicia');
+ expect(store.users.size).toBe(2);
+ expect(store.tags.size).toBe(2);
+ expect(alice.date).toBe(date);
+ expect(store.opaque).toBe(opaque);
+ expect(beforeHydration.profile.greeting).toBe('Hello, Alice');
+ expect(beforeHydration.users._entries).toEqual([
+ ['alice', {name: 'Alice', date}],
+ ]);
+ expect(beforeHydration.users.size).toBe(1);
+ expect(beforeHydration.tags.size).toBe(1);
+ } finally {
+ handle.unsubscribe();
+ }
+ });
+
+ it('preserves own accessors returned by a custom merge', async () => {
+ function createProfile(name: string) {
+ return {
+ name,
+ get greeting() {
+ return `Hello, ${this.name}`;
+ },
+ };
+ }
+
+ const storage = createMockStorage();
+ storage.data.set(
+ 'test',
+ JSON.stringify({version: 0, state: {profile: {name: 'Alice'}}}),
+ );
+ const store = createClassyStore({
+ profile: createProfile('default'),
+ });
+ const handle = persist(store, {
+ name: 'test',
+ storage,
+ merge: (persisted) => ({
+ profile: createProfile((persisted.profile as {name: string}).name),
+ }),
+ });
+
+ try {
+ await handle.hydrated;
+ expect(store.profile.greeting).toBe('Hello, Alice');
+ store.profile.name = 'Alicia';
+ expect(store.profile.greeting).toBe('Hello, Alicia');
+ } finally {
+ handle.unsubscribe();
+ }
+ });
+
it('shallow merge (default): preserves new properties not in storage', async () => {
const storage = createMockStorage();
storage.data.set(
@@ -1165,6 +1327,93 @@ describe('persist()', () => {
});
});
+ describe('pending hydration cleanup', () => {
+ it.each([
+ false,
+ true,
+ ])('ignores a pending read after unsubscribe (skipHydration=%s)', async (skipHydration) => {
+ let resolveRead!: (value: string | null) => void;
+ const pendingRead = new Promise((resolve) => {
+ resolveRead = resolve;
+ });
+ const storage = {
+ getItem: () => pendingRead,
+ setItem: mock(() => {}),
+ removeItem: mock(() => {}),
+ };
+ const store = createClassyStore({title: 'Current'});
+ const handle = persist(store, {name: 'editor', storage, skipHydration});
+ const completion = skipHydration ? handle.rehydrate() : handle.hydrated;
+ handle.unsubscribe();
+ resolveRead(JSON.stringify({version: 0, state: {title: 'Retired'}}));
+ await completion;
+ await handle.hydrated;
+
+ expect(store.title).toBe('Current');
+ expect(storage.setItem).not.toHaveBeenCalled();
+ expect(storage.removeItem).not.toHaveBeenCalled();
+ expect(handle.isHydrated).toBe(true);
+ });
+
+ it.each([
+ false,
+ true,
+ ])('settles a failed pending read without callbacks after unsubscribe (skipHydration=%s)', async (skipHydration) => {
+ let rejectRead!: (error: Error) => void;
+ const pendingRead = new Promise((_resolve, reject) => {
+ rejectRead = reject;
+ });
+ const onError = mock(() => {});
+ const storage = {
+ getItem: () => pendingRead,
+ setItem: mock(() => {}),
+ removeItem: mock(() => {}),
+ };
+ const store = createClassyStore({title: 'Current'});
+ const handle = persist(store, {
+ name: 'editor',
+ storage,
+ skipHydration,
+ onError,
+ });
+ const completion = skipHydration ? handle.rehydrate() : handle.hydrated;
+ handle.unsubscribe();
+ rejectRead(new Error('Late storage failure'));
+ await completion;
+ await handle.hydrated;
+
+ expect(onError).not.toHaveBeenCalled();
+ expect(store.title).toBe('Current');
+ expect(handle.isHydrated).toBe(true);
+ });
+
+ it('does not clear storage from an expired read that finishes after cleanup', async () => {
+ let resolveRead!: (value: string) => void;
+ const storage = {
+ getItem: () =>
+ new Promise((resolve) => {
+ resolveRead = resolve;
+ }),
+ setItem: mock(() => {}),
+ removeItem: mock(() => {}),
+ };
+ const store = createClassyStore({title: 'Current'});
+ const handle = persist(store, {
+ name: 'editor',
+ storage,
+ clearOnExpire: true,
+ });
+ handle.unsubscribe();
+ resolveRead(
+ JSON.stringify({version: 0, expiresAt: 0, state: {title: 'Expired'}}),
+ );
+ await handle.hydrated;
+
+ expect(storage.removeItem).not.toHaveBeenCalled();
+ expect(handle.isExpired).toBe(false);
+ });
+ });
+
// ββ onError callback ββββββββββββββββββββββββββββββββββββββββββββββββββββ
describe('onError callback', () => {
diff --git a/packages/classy-store/src/utils/persist/persist.ts b/packages/classy-store/src/utils/persist/persist.ts
index 8e12c2d..53a9b2e 100644
--- a/packages/classy-store/src/utils/persist/persist.ts
+++ b/packages/classy-store/src/utils/persist/persist.ts
@@ -1,5 +1,6 @@
-import {subscribe} from '../../core/core';
+import {getVersion, subscribe} from '../../core/core';
import {snapshot} from '../../snapshot/snapshot';
+import {cloneSnapshotValue} from '../cloneSnapshot/cloneSnapshot';
import {findGetterDescriptor} from '../internal/internal';
// ββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -151,7 +152,11 @@ export type PersistOperation = 'write' | 'read' | 'remove' | 'parse';
* Handle returned by `persist()`. Provides control over the persist lifecycle.
*/
export type PersistHandle = {
- /** Stop persisting and clean up (unsubscribe + cancel pending debounce + remove storage event listener). */
+ /**
+ * Stop persisting, cancel pending debounce, and remove the storage listener.
+ * Pending read results/errors and unstarted queued writes are ignored.
+ * Their promises still settle; already-issued adapter I/O is not cancelled.
+ */
unsubscribe: () => void;
/** Promise that resolves when initial hydration from storage is complete. */
@@ -160,13 +165,13 @@ export type PersistHandle = {
/** Whether hydration from storage has completed. */
isHydrated: boolean;
- /** Manually trigger a write to storage (bypasses debounce). */
+ /** Save current state, bypassing debounce and waiting behind earlier writes. */
save: () => Promise;
- /** Clear this store's persisted data from storage. */
+ /** Cancel pending auto-saves and remove persisted data after earlier writes. */
clear: () => Promise;
- /** Manually re-hydrate the store from storage. */
+ /** Re-read after prior writes; ignore the result if a newer read/edit supersedes it. */
rehydrate: () => Promise;
/** True if the last hydration found expired data (requires `expireIn`). */
@@ -280,6 +285,9 @@ export function persist(
let debounceTimer: ReturnType | null = null;
let hydratedFlag = false;
let expiredFlag = false;
+ let hydrationRequest = 0;
+ let storageQueue: Promise | undefined;
+ let clearedVersion: number | undefined;
// Hydration promise + resolver.
let resolveHydrated!: () => void;
@@ -299,6 +307,28 @@ export function persist(
}
const propKeys = resolvedProps.map((p) => p.key);
+ /** Keep this handle's writes/removals in order, even with an async adapter. */
+ function enqueueStorageMutation(
+ operation: () => void | Promise,
+ kind: 'write' | 'remove',
+ ): Promise {
+ const execute = async () => {
+ if (disposed) return;
+ try {
+ await operation();
+ } catch (error) {
+ reportError(error, kind);
+ }
+ };
+ // Start the first operation immediately for synchronous storage adapters.
+ const pending = storageQueue ? storageQueue.then(execute) : execute();
+ const completion = pending.finally(() => {
+ if (storageQueue === completion) storageQueue = undefined;
+ });
+ storageQueue = completion;
+ return completion;
+ }
+
// ββ Save logic βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/** Serialize the current store state into a JSON string (versioned envelope). */
@@ -325,9 +355,11 @@ export function persist(
/** Write the current state to storage. */
async function writeToStorage(): Promise {
if (disposed) return;
+ hydrationRequest++;
try {
+ // Capture now; a queued save must represent the state requested by its caller.
const json = serializeState();
- await storage.setItem(name, json);
+ await enqueueStorageMutation(() => storage.setItem(name, json), 'write');
} catch (error) {
reportError(error, 'write');
}
@@ -336,6 +368,8 @@ export function persist(
/** Schedule a debounced write (or write immediately if debounce is 0). */
function scheduleWrite(): void {
if (disposed || hydrating) return;
+ // clear() can run before a mutation's batched notification is delivered.
+ if (getVersion(proxyStore) === clearedVersion) return;
if (debounceMs <= 0) {
void writeToStorage();
@@ -382,9 +416,7 @@ export function persist(
) {
expiredFlag = true;
if (clearOnExpire) {
- void Promise.resolve(storage.removeItem(name)).catch((error) => {
- reportError(error, 'remove');
- });
+ void enqueueStorageMutation(() => storage.removeItem(name), 'remove');
}
return;
}
@@ -421,23 +453,41 @@ export function persist(
merged = {...currentState, ...state};
}
+ // Merged defaults may still reference frozen snapshot branches. Restore
+ // mutable copies without changing snapshots retained by callers or merges.
+ const clones = new WeakMap();
// Apply to store proxy (goes through SET traps β reactivity).
for (const key of propKeys) {
if (key in merged) {
- (proxyStore as Record)[key] = merged[key];
+ (proxyStore as Record)[key] = cloneSnapshotValue(
+ merged[key],
+ clones,
+ );
}
}
}
/** Read from storage and apply to the store. */
async function hydrateFromStorage(): Promise {
+ const request = ++hydrationRequest;
+ const storeVersion = getVersion(proxyStore);
+ const isCurrent = () =>
+ !disposed &&
+ request === hydrationRequest &&
+ storeVersion === getVersion(proxyStore);
let raw: string | null;
try {
+ // A read requested after save()/clear() observes those operations' results.
+ if (storageQueue) await storageQueue;
+ if (!isCurrent()) return;
raw = await storage.getItem(name);
} catch (error) {
- reportError(error, 'read');
+ if (isCurrent()) reportError(error, 'read');
return;
}
+ // Cleanup, a newer request, or a local edit can supersede an in-flight read.
+ // Obsolete data must not replace current state or clear newer stored data.
+ if (!isCurrent()) return;
if (raw !== null) {
// A debounced local write may be pending from a mutation issued before
// we got around to hydrating; the freshly-loaded state would be
@@ -447,11 +497,13 @@ export function persist(
debounceTimer = null;
}
hydrating = true;
- applyPersistedState(raw);
- // Reset after microtask so the batched subscription callback
- // (which fires via queueMicrotask) still sees the flag as true.
- await new Promise((r) => queueMicrotask(r));
- hydrating = false;
+ try {
+ applyPersistedState(raw);
+ } finally {
+ // Keep the batched notification from writing restored state back.
+ await new Promise((resolve) => queueMicrotask(resolve));
+ hydrating = false;
+ }
}
}
@@ -462,6 +514,11 @@ export function persist(
if (disposed) return;
if (event.key !== name) return;
if (event.newValue === null) return; // cleared
+ // Delivery is asynchronous: another tab or a local save may have already
+ // replaced this event's value. Do not roll the UI back to an obsolete value.
+ if (event.storageArea && event.storageArea.getItem(name) !== event.newValue)
+ return;
+ hydrationRequest++;
// A pending local debounced write would otherwise overwrite the remote
// state once its timer fires.
if (debounceTimer !== null) {
@@ -546,11 +603,13 @@ export function persist(
async clear() {
if (disposed) return;
- try {
- await storage.removeItem(name);
- } catch (error) {
- reportError(error, 'remove');
+ hydrationRequest++;
+ clearedVersion = getVersion(proxyStore);
+ if (debounceTimer !== null) {
+ clearTimeout(debounceTimer);
+ debounceTimer = null;
}
+ await enqueueStorageMutation(() => storage.removeItem(name), 'remove');
},
async rehydrate() {
diff --git a/packages/classy-store/tests/browser/README.md b/packages/classy-store/tests/browser/README.md
new file mode 100644
index 0000000..409b2d7
--- /dev/null
+++ b/packages/classy-store/tests/browser/README.md
@@ -0,0 +1,35 @@
+# React browser regressions
+
+From the repository root:
+
+```sh
+bun install --frozen-lockfile
+cd packages/classy-store
+bunx playwright install chromium firefox webkit
+bun run test:browser
+```
+
+CI also installs the Linux system dependencies with `playwright install --with-deps`. The suite starts an isolated local Bun server and tests the source React binding in Chromium, Firefox, and WebKit. Package exports and React version compatibility are tested separately in `tests/compatibility`.
+
+Every test fails on browser console errors or uncaught page errors. Native clipboard replacement and subsequent caret behavior are tested in Chromium; Firefox and WebKit explicitly skip those cases because they do not expose the same clipboard permissions API.
+
+The `/persistence` fixture opens two pages in one browser context and uses real
+`localStorage` events. It verifies bidirectional input updates for selectors and
+auto-tracking, no echo writes, reload restoration, unsubscribe, remote updates
+cancelling pending debounces, and a newer local save winning over an older queued
+storage event. The conflict case uses a capture listener to make the ordering
+deterministic; the event itself is browser-generated and must be trusted. Errors
+are collected from both tabs. These cases run in the existing three-browser CI
+matrix.
+
+The four hook forms (selector, auto-tracked, bound selector, and bound auto-tracked) cover middle insertion, selection replacement, deletion, multiline and Unicode input, checkbox changes, composition event sequences, and multi-property/array updates. The latter asserts one React commit in the event handler and one ordinary subscription callback after the microtask flush.
+
+Composition tests combine synthetic boundaries with browser text insertion. They do not automate a native operating-system IME candidate picker; that still needs a manual smoke test. Firefox emits additional composition boundaries during text insertion, so the suite checks balanced boundaries and the final value/caret rather than a browser-specific event count.
+
+The caret tests failed under the previous default batched React subscriptions: inserting `XY` in `abcdef` at position 2 produced `abXcdefY`. No `sync` option is passed by any fixture.
+
+Hydration regressions render both a complete server response and a real `renderToReadableStream` response with separate shell and Suspense chunks. A client gate suspends before any child store hook runs. The parent effect restores real `localStorage` persistence while that boundary remains dehydrated; the tests then require all four hook forms to hydrate from the prepared server snapshot without a recoverable error or replacing any server input/output element. After hydration they must show the restored state and handle live input with its caret intact.
+
+The streaming test starts the client hydration script explicitly after observing the shell, then releases the server chunk through a request-scoped test endpoint. This controls the ordering without timing sleeps and avoids differences in browser bootstrap-script scheduling during an open response. Client and server gates are independent. The complete response uses an ordinary script tag. Before `prepareStoreForHydration`, the completed-response regression failed with a hydration mismatch, an initial read of persisted state instead of server state, and replaced DOM.
+
+The `/compiler` fixture transforms six consumer components with React Compiler target `19` before Bun bundles them. The shared compiler helper requires named `CompileSuccess` events with memo slots and a compiler runtime import, so skipped compilation fails the suite. All four hook forms cover controlled-input caret position, multi-property batching, local re-renders, conditional nested reads, and automatically memoized children. Before the tracking fix, changing only a memoized child's nested field after a parent-only render left both auto-tracked modes stale; selector modes passed.
diff --git a/packages/classy-store/tests/browser/browser.store.ts b/packages/classy-store/tests/browser/browser.store.ts
new file mode 100644
index 0000000..b97fdcb
--- /dev/null
+++ b/packages/classy-store/tests/browser/browser.store.ts
@@ -0,0 +1,42 @@
+import {createClassyStore, subscribe} from '../../src/core/core';
+import {createStoreHook} from '../../src/frameworks/react/react';
+
+class InputStore {
+ text = 'abcdef';
+ notes = 'first line';
+ checked = false;
+ edits = 0;
+ first = 'First';
+ last = 'Last';
+ items = ['a', 'b', 'c'];
+
+ updateTogether() {
+ this.first = 'Ada';
+ this.last = 'Lovelace';
+ this.items.splice(0, 2, 'x');
+ this.items.push('z');
+ this.items.reverse();
+ }
+}
+
+export const inputStore = createClassyStore(new InputStore());
+export const useInputStore = createStoreHook(inputStore);
+export const browserProbe = {
+ notifications: 0,
+ notificationsDuringEvent: -1,
+ commits: [] as string[],
+ eventValues: [] as string[],
+ compositions: [] as string[],
+};
+
+subscribe(inputStore, () => {
+ browserProbe.notifications++;
+});
+
+declare global {
+ interface Window {
+ browserProbe: typeof browserProbe;
+ }
+}
+
+window.browserProbe = browserProbe;
diff --git a/packages/classy-store/tests/browser/browser.test.ts b/packages/classy-store/tests/browser/browser.test.ts
new file mode 100644
index 0000000..e0701a6
--- /dev/null
+++ b/packages/classy-store/tests/browser/browser.test.ts
@@ -0,0 +1,21 @@
+import {test as base, expect, type Page} from '@playwright/test';
+
+export const test = base.extend<{browserErrors: undefined}>({
+ browserErrors: [
+ async ({page, context}, use) => {
+ const errors: string[] = [];
+ function observe(tab: Page) {
+ tab.on('pageerror', (error) => errors.push(error.message));
+ tab.on('console', (message) => {
+ if (message.type() === 'error') errors.push(message.text());
+ });
+ }
+ observe(page);
+ context.on('page', observe);
+ await use(undefined);
+ context.off('page', observe);
+ expect(errors, 'Browser exceptions and console errors').toEqual([]);
+ },
+ {auto: true},
+ ],
+});
diff --git a/packages/classy-store/tests/browser/compiler.spec.ts b/packages/classy-store/tests/browser/compiler.spec.ts
new file mode 100644
index 0000000..a28de8b
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler.spec.ts
@@ -0,0 +1,85 @@
+import {expect} from '@playwright/test';
+import {test} from './browser.test';
+
+for (const mode of ['selector', 'tracked', 'bound-selector', 'bound-tracked']) {
+ test.describe(`compiled ${mode}`, () => {
+ test.beforeEach(async ({page}) => {
+ await page.goto(`/compiler?mode=${mode}`);
+ await expect(page.getByLabel('Compiled text')).toHaveValue('abcdef');
+ await expect(page.getByTestId('memo-profile')).toHaveText('Lovelace');
+ });
+
+ test('keeps controlled input values and caret after a local rerender', async ({
+ page,
+ }) => {
+ await page.getByRole('button', {name: 'Local rerender'}).click();
+ await expect(page.getByTestId('local-revision')).toHaveText('1');
+ await expect(page.getByTestId('label')).toHaveText('one');
+ const input = page.getByLabel('Compiled text');
+ await input.focus();
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 2),
+ );
+ await input.pressSequentially('XY');
+ await expect(input).toHaveValue('abXYcdef');
+ await expect(page.getByTestId('compiled-text')).toHaveText('abXYcdef');
+ await expect(page.getByTestId('compiled-edits')).toHaveText('2');
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(4);
+ expect(await page.evaluate(() => window.compilerProbe)).toEqual({
+ notifications: 1,
+ notificationsDuringEvent: 0,
+ });
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 4),
+ );
+ await input.pressSequentially('Z');
+ await expect(input).toHaveValue('abZcdef');
+ await expect(page.getByTestId('compiled-edits')).toHaveText('3');
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(3);
+ });
+
+ test('retains cached reads and follows conditional nested reads and memoized children', async ({
+ page,
+ }) => {
+ await page.getByRole('button', {name: 'Local rerender'}).click();
+ await expect(page.getByTestId('label')).toHaveText('one');
+ await page.evaluate(() => {
+ window.compilerStore.profile.last = 'King';
+ });
+ await expect(page.getByTestId('memo-profile')).toHaveText('King');
+ await page.evaluate(() => {
+ window.compilerStore.profile.first = 'Augusta';
+ });
+ await expect(page.getByTestId('conditional-name')).toHaveText('Augusta');
+ await page.getByRole('button', {name: 'Toggle branch'}).click();
+ await expect(page.getByTestId('conditional-name')).toHaveText('Grace');
+ await page.getByRole('button', {name: 'Local rerender'}).click();
+ await expect(page.getByTestId('label')).toHaveText('zero');
+ await page.evaluate(() => {
+ window.compilerStore.fallback.name = 'Hopper';
+ });
+ await expect(page.getByTestId('conditional-name')).toHaveText('Hopper');
+ await page.evaluate(() => {
+ window.compilerStore.profile = {first: 'Katherine', last: 'Johnson'};
+ });
+ await expect(page.getByTestId('memo-profile')).toHaveText('Johnson');
+ await page.getByRole('button', {name: 'Toggle branch'}).click();
+ await expect(page.getByTestId('conditional-name')).toHaveText(
+ 'Katherine',
+ );
+ await page.evaluate(() => {
+ window.compilerStore.text = 'external';
+ });
+ await expect(page.getByLabel('Compiled text')).toHaveValue('external');
+ await expect(page.getByTestId('compiled-text')).toHaveText('external');
+ });
+ });
+}
diff --git a/packages/classy-store/tests/browser/compiler/client.tsx b/packages/classy-store/tests/browser/compiler/client.tsx
new file mode 100644
index 0000000..6d58f49
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/client.tsx
@@ -0,0 +1,17 @@
+import {createRoot} from 'react-dom/client';
+import {CompilerBoundSelector} from './components/compilerBoundSelector/CompilerBoundSelector';
+import {CompilerBoundTracked} from './components/compilerBoundTracked/CompilerBoundTracked';
+import {CompilerSelector} from './components/compilerSelector/CompilerSelector';
+import {CompilerTracked} from './components/compilerTracked/CompilerTracked';
+
+const forms = {
+ selector: CompilerSelector,
+ tracked: CompilerTracked,
+ 'bound-selector': CompilerBoundSelector,
+ 'bound-tracked': CompilerBoundTracked,
+};
+const mode = new URLSearchParams(location.search).get('mode') ?? 'selector';
+const Form = forms[mode as keyof typeof forms];
+const container = document.getElementById('compiler-root');
+if (!container || !Form) throw new Error('Missing compiler fixture');
+createRoot(container).render();
diff --git a/packages/classy-store/tests/browser/compiler/compiler.store.ts b/packages/classy-store/tests/browser/compiler/compiler.store.ts
new file mode 100644
index 0000000..f1e7d1c
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/compiler.store.ts
@@ -0,0 +1,54 @@
+import {createClassyStore, subscribe} from '../../../src/core/core';
+import {createStoreHook} from '../../../src/frameworks/react/react';
+import type {Snapshot} from '../../../src/types';
+
+class CompilerStore {
+ text = 'abcdef';
+ edits = 0;
+ profile = {first: 'Ada', last: 'Lovelace'};
+ fallback = {name: 'Grace'};
+ labels = ['zero', 'one'];
+ unused = 0;
+}
+
+export const compilerStore = createClassyStore(new CompilerStore());
+export const useCompilerStore = createStoreHook(compilerStore);
+export const compilerProbe = {
+ notifications: 0,
+ notificationsDuringEvent: -1,
+};
+
+subscribe(compilerStore, () => compilerProbe.notifications++);
+
+export function selectCompilerFields(state: Snapshot) {
+ return {
+ text: state.text,
+ edits: state.edits,
+ profile: state.profile,
+ fallback: state.fallback,
+ labels: state.labels,
+ };
+}
+
+export function equalCompilerFields(
+ previous: ReturnType,
+ next: ReturnType,
+) {
+ return (
+ previous.text === next.text &&
+ previous.edits === next.edits &&
+ previous.profile === next.profile &&
+ previous.fallback === next.fallback &&
+ previous.labels === next.labels
+ );
+}
+
+declare global {
+ interface Window {
+ compilerStore: typeof compilerStore;
+ compilerProbe: typeof compilerProbe;
+ }
+}
+
+window.compilerStore = compilerStore;
+window.compilerProbe = compilerProbe;
diff --git a/packages/classy-store/tests/browser/compiler/components/compilerBoundSelector/CompilerBoundSelector.tsx b/packages/classy-store/tests/browser/compiler/components/compilerBoundSelector/CompilerBoundSelector.tsx
new file mode 100644
index 0000000..4a06cf0
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/components/compilerBoundSelector/CompilerBoundSelector.tsx
@@ -0,0 +1,40 @@
+import {useState} from 'react';
+import {
+ equalCompilerFields,
+ selectCompilerFields,
+ useCompilerStore,
+} from '../../compiler.store';
+import {CompilerFields} from '../compilerFields/CompilerFields';
+import {MemoProfile} from '../memoProfile/MemoProfile';
+
+export function CompilerBoundSelector() {
+ const [revision, setRevision] = useState(0);
+ const [showFallback, setShowFallback] = useState(false);
+ const state = useCompilerStore(selectCompilerFields, {
+ isEqual: equalCompilerFields,
+ });
+ const name = showFallback ? state.fallback.name : state.profile.first;
+ const label = state.labels[revision % 2];
+
+ return (
+
+ setRevision((previous) => previous + 1)}
+ >
+ Local rerender
+
+ setShowFallback((previous) => !previous)}
+ >
+ Toggle branch
+
+ {revision}
+ {label}
+ {name}
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/compiler/components/compilerBoundTracked/CompilerBoundTracked.tsx b/packages/classy-store/tests/browser/compiler/components/compilerBoundTracked/CompilerBoundTracked.tsx
new file mode 100644
index 0000000..9436aed
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/components/compilerBoundTracked/CompilerBoundTracked.tsx
@@ -0,0 +1,34 @@
+import {useState} from 'react';
+import {useCompilerStore} from '../../compiler.store';
+import {CompilerFields} from '../compilerFields/CompilerFields';
+import {MemoProfile} from '../memoProfile/MemoProfile';
+
+export function CompilerBoundTracked() {
+ const [revision, setRevision] = useState(0);
+ const [showFallback, setShowFallback] = useState(false);
+ const state = useCompilerStore();
+ const name = showFallback ? state.fallback.name : state.profile.first;
+ const label = state.labels[revision % 2];
+
+ return (
+
+ setRevision((previous) => previous + 1)}
+ >
+ Local rerender
+
+ setShowFallback((previous) => !previous)}
+ >
+ Toggle branch
+
+ {revision}
+ {label}
+ {name}
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/compiler/components/compilerFields/CompilerFields.tsx b/packages/classy-store/tests/browser/compiler/components/compilerFields/CompilerFields.tsx
new file mode 100644
index 0000000..b39e04c
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/components/compilerFields/CompilerFields.tsx
@@ -0,0 +1,25 @@
+import {compilerProbe, compilerStore} from '../../compiler.store';
+
+type Props = {text: string; edits: number};
+
+export function CompilerFields({text, edits}: Props) {
+ return (
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/compiler/components/compilerSelector/CompilerSelector.tsx b/packages/classy-store/tests/browser/compiler/components/compilerSelector/CompilerSelector.tsx
new file mode 100644
index 0000000..c4b2331
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/components/compilerSelector/CompilerSelector.tsx
@@ -0,0 +1,41 @@
+import {useState} from 'react';
+import {useClassyStore} from '../../../../../src/frameworks/react/react';
+import {
+ compilerStore,
+ equalCompilerFields,
+ selectCompilerFields,
+} from '../../compiler.store';
+import {CompilerFields} from '../compilerFields/CompilerFields';
+import {MemoProfile} from '../memoProfile/MemoProfile';
+
+export function CompilerSelector() {
+ const [revision, setRevision] = useState(0);
+ const [showFallback, setShowFallback] = useState(false);
+ const state = useClassyStore(compilerStore, selectCompilerFields, {
+ isEqual: equalCompilerFields,
+ });
+ const name = showFallback ? state.fallback.name : state.profile.first;
+ const label = state.labels[revision % 2];
+
+ return (
+
+ setRevision((previous) => previous + 1)}
+ >
+ Local rerender
+
+ setShowFallback((previous) => !previous)}
+ >
+ Toggle branch
+
+ {revision}
+ {label}
+ {name}
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/compiler/components/compilerTracked/CompilerTracked.tsx b/packages/classy-store/tests/browser/compiler/components/compilerTracked/CompilerTracked.tsx
new file mode 100644
index 0000000..d3ae842
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/components/compilerTracked/CompilerTracked.tsx
@@ -0,0 +1,35 @@
+import {useState} from 'react';
+import {useClassyStore} from '../../../../../src/frameworks/react/react';
+import {compilerStore} from '../../compiler.store';
+import {CompilerFields} from '../compilerFields/CompilerFields';
+import {MemoProfile} from '../memoProfile/MemoProfile';
+
+export function CompilerTracked() {
+ const [revision, setRevision] = useState(0);
+ const [showFallback, setShowFallback] = useState(false);
+ const state = useClassyStore(compilerStore);
+ const name = showFallback ? state.fallback.name : state.profile.first;
+ const label = state.labels[revision % 2];
+
+ return (
+
+ setRevision((previous) => previous + 1)}
+ >
+ Local rerender
+
+ setShowFallback((previous) => !previous)}
+ >
+ Toggle branch
+
+ {revision}
+ {label}
+ {name}
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/compiler/components/memoProfile/MemoProfile.tsx b/packages/classy-store/tests/browser/compiler/components/memoProfile/MemoProfile.tsx
new file mode 100644
index 0000000..c7ff2c3
--- /dev/null
+++ b/packages/classy-store/tests/browser/compiler/components/memoProfile/MemoProfile.tsx
@@ -0,0 +1,5 @@
+type Props = {profile: Readonly<{first: string; last: string}>};
+
+export function MemoProfile({profile}: Props) {
+ return {profile.last} ;
+}
diff --git a/packages/classy-store/tests/browser/components/boundSelectorForm/BoundSelectorForm.tsx b/packages/classy-store/tests/browser/components/boundSelectorForm/BoundSelectorForm.tsx
new file mode 100644
index 0000000..5a26eb7
--- /dev/null
+++ b/packages/classy-store/tests/browser/components/boundSelectorForm/BoundSelectorForm.tsx
@@ -0,0 +1,7 @@
+import {useInputStore} from '../../browser.store';
+import {InputForm} from '../inputForm/InputForm';
+
+export function BoundSelectorForm() {
+ const state = useInputStore((snapshot) => snapshot);
+ return ;
+}
diff --git a/packages/classy-store/tests/browser/components/boundTrackedForm/BoundTrackedForm.tsx b/packages/classy-store/tests/browser/components/boundTrackedForm/BoundTrackedForm.tsx
new file mode 100644
index 0000000..a23e827
--- /dev/null
+++ b/packages/classy-store/tests/browser/components/boundTrackedForm/BoundTrackedForm.tsx
@@ -0,0 +1,7 @@
+import {useInputStore} from '../../browser.store';
+import {InputForm} from '../inputForm/InputForm';
+
+export function BoundTrackedForm() {
+ const state = useInputStore();
+ return ;
+}
diff --git a/packages/classy-store/tests/browser/components/derivedForm/DerivedForm.tsx b/packages/classy-store/tests/browser/components/derivedForm/DerivedForm.tsx
new file mode 100644
index 0000000..03b9d71
--- /dev/null
+++ b/packages/classy-store/tests/browser/components/derivedForm/DerivedForm.tsx
@@ -0,0 +1,74 @@
+import {useLayoutEffect} from 'react';
+import {createClassyStore, subscribe} from '../../../../src/core/core';
+import {
+ createStoreHook,
+ useClassyStore,
+} from '../../../../src/frameworks/react/react';
+
+class DerivedStore {
+ selectedId = 1;
+ items = [
+ {id: 1, name: 'Ada'},
+ {id: 2, name: 'Grace'},
+ {id: 3, name: 'Linus'},
+ ];
+
+ get selectedName() {
+ // Valid before and after replaceSelection, but not between its writes.
+ const selected = this.items.find((item) => item.id === this.selectedId);
+ if (!selected) throw new Error('Selected item is temporarily absent');
+ return selected.name;
+ }
+
+ get names() {
+ return this.items.map((item) => item.name).join(',');
+ }
+
+ replaceSelection() {
+ this.items.splice(0, 2, {id: 4, name: 'Hedy'});
+ this.selectedId = 4;
+ this.items.push({id: 5, name: 'Katherine'});
+ this.items.reverse();
+ }
+}
+
+const store = createClassyStore(new DerivedStore());
+const useStore = createStoreHook(store);
+let ordinaryNotifications = 0;
+subscribe(store, () => ordinaryNotifications++);
+
+export function DerivedForm() {
+ const selected = useClassyStore(store, (state) => state.selectedName);
+ const tracked = useClassyStore(store);
+ const boundSelected = useStore((state) => state.names);
+ const boundTracked = useStore();
+ const values = [
+ selected,
+ tracked.selectedName,
+ boundSelected,
+ boundTracked.names,
+ ].join('|');
+
+ useLayoutEffect(() => {
+ const output = document.getElementById('derived-commits');
+ if (output) output.textContent += `${values}\n`;
+ });
+
+ return (
+
+ {values}
+
+
+ {
+ store.replaceSelection();
+ const output = document.getElementById('notifications-during-event');
+ if (output) output.textContent = String(ordinaryNotifications);
+ }}
+ >
+ Replace selection
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/components/inputForm/InputForm.tsx b/packages/classy-store/tests/browser/components/inputForm/InputForm.tsx
new file mode 100644
index 0000000..ae3c3ac
--- /dev/null
+++ b/packages/classy-store/tests/browser/components/inputForm/InputForm.tsx
@@ -0,0 +1,67 @@
+import {useLayoutEffect} from 'react';
+import type {Snapshot} from '../../../../src/types';
+import {browserProbe, inputStore} from '../../browser.store';
+
+type Props = {state: Snapshot};
+
+export function InputForm({state}: Props) {
+ const fullName = `${state.first} ${state.last}`;
+ const items = state.items.join(',');
+ useLayoutEffect(() => {
+ browserProbe.commits.push(`${fullName}:${items}`);
+ });
+
+ return (
+
+
+ Text
+ {
+ inputStore.text = event.target.value;
+ inputStore.edits++;
+ browserProbe.eventValues.push(event.target.value);
+ }}
+ onCompositionStart={() => browserProbe.compositions.push('start')}
+ onCompositionEnd={() => browserProbe.compositions.push('end')}
+ />
+
+
+ Notes
+
+
+ Enabled
+ {
+ inputStore.checked = event.target.checked;
+ }}
+ />
+
+ {state.text}
+ {state.notes}
+ {String(state.checked)}
+ {state.edits}
+ {fullName}
+ {items}
+ {
+ browserProbe.commits.length = 0;
+ browserProbe.notifications = 0;
+ inputStore.updateTogether();
+ browserProbe.notificationsDuringEvent = browserProbe.notifications;
+ }}
+ >
+ Update together
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/components/selectorForm/SelectorForm.tsx b/packages/classy-store/tests/browser/components/selectorForm/SelectorForm.tsx
new file mode 100644
index 0000000..a6cf61c
--- /dev/null
+++ b/packages/classy-store/tests/browser/components/selectorForm/SelectorForm.tsx
@@ -0,0 +1,8 @@
+import {useClassyStore} from '../../../../src/frameworks/react/react';
+import {inputStore} from '../../browser.store';
+import {InputForm} from '../inputForm/InputForm';
+
+export function SelectorForm() {
+ const state = useClassyStore(inputStore, (snapshot) => snapshot);
+ return ;
+}
diff --git a/packages/classy-store/tests/browser/components/trackedForm/TrackedForm.tsx b/packages/classy-store/tests/browser/components/trackedForm/TrackedForm.tsx
new file mode 100644
index 0000000..fe48661
--- /dev/null
+++ b/packages/classy-store/tests/browser/components/trackedForm/TrackedForm.tsx
@@ -0,0 +1,8 @@
+import {useClassyStore} from '../../../../src/frameworks/react/react';
+import {inputStore} from '../../browser.store';
+import {InputForm} from '../inputForm/InputForm';
+
+export function TrackedForm() {
+ const state = useClassyStore(inputStore);
+ return ;
+}
diff --git a/packages/classy-store/tests/browser/frontend.tsx b/packages/classy-store/tests/browser/frontend.tsx
new file mode 100644
index 0000000..d8ecc39
--- /dev/null
+++ b/packages/classy-store/tests/browser/frontend.tsx
@@ -0,0 +1,19 @@
+import {createRoot} from 'react-dom/client';
+import {BoundSelectorForm} from './components/boundSelectorForm/BoundSelectorForm';
+import {BoundTrackedForm} from './components/boundTrackedForm/BoundTrackedForm';
+import {DerivedForm} from './components/derivedForm/DerivedForm';
+import {SelectorForm} from './components/selectorForm/SelectorForm';
+import {TrackedForm} from './components/trackedForm/TrackedForm';
+
+const forms = {
+ derived: DerivedForm,
+ selector: SelectorForm,
+ tracked: TrackedForm,
+ 'bound-selector': BoundSelectorForm,
+ 'bound-tracked': BoundTrackedForm,
+};
+const mode = new URLSearchParams(location.search).get('mode') ?? 'selector';
+const Form = forms[mode as keyof typeof forms];
+const container = document.getElementById('root');
+if (!container || !Form) throw new Error('Missing browser fixture');
+createRoot(container).render();
diff --git a/packages/classy-store/tests/browser/hydration.spec.ts b/packages/classy-store/tests/browser/hydration.spec.ts
new file mode 100644
index 0000000..20d9b3b
--- /dev/null
+++ b/packages/classy-store/tests/browser/hydration.spec.ts
@@ -0,0 +1,124 @@
+import {expect} from '@playwright/test';
+import {test} from './browser.test';
+import {hydrationStorageKey} from './hydration/hydration.store';
+
+for (const rendering of ['complete', 'streaming']) {
+ test(`${rendering}: hydrates a delayed Suspense boundary from server state after parent persistence restores`, async ({
+ page,
+ }) => {
+ await page.addInitScript(
+ ({storageKey}) => {
+ localStorage.setItem(
+ storageKey,
+ JSON.stringify({
+ version: 0,
+ state: {user: {name: 'Saved'}, count: 8},
+ }),
+ );
+ },
+ {storageKey: hydrationStorageKey},
+ );
+ const streamId = crypto.randomUUID();
+ await page.goto(
+ rendering === 'streaming'
+ ? `/hydration/stream?id=${streamId}`
+ : '/hydration',
+ {waitUntil: 'commit'},
+ );
+ if (rendering === 'streaming') {
+ await expect(page.getByText('Loading fields')).toBeVisible();
+ // Start hydration at a deterministic shell stage. Browser scheduling of
+ // bootstrap scripts while a response remains open differs across engines.
+ await page.addScriptTag({url: '/hydration/client.js'});
+ }
+ await page.waitForFunction(() => window.hydrationRuntime?.probe.restored);
+
+ expect(
+ await page.evaluate(() => ({
+ name: window.hydrationRuntime.store.user.name,
+ parentHydrated: window.hydrationRuntime.probe.parentHydrated,
+ childHydrated: window.hydrationRuntime.probe.childHydrated,
+ valuesRead: window.hydrationRuntime.probe.valuesRead,
+ })),
+ ).toEqual({
+ name: 'Saved',
+ parentHydrated: true,
+ childHydrated: false,
+ valuesRead: [],
+ });
+ if (rendering === 'streaming') {
+ await expect(page.getByText('Loading fields')).toBeVisible();
+ const response = await page.request.post(
+ `/hydration/release?id=${streamId}`,
+ );
+ expect(response.ok()).toBe(true);
+ await expect(page.getByTestId('hydration-selector')).toHaveText(
+ 'Server:1',
+ );
+ await page.evaluate(() => {
+ window.hydrationRuntime.probe.serverElements = [
+ ...document.querySelectorAll(
+ '#hydration-root output, #hydration-root input',
+ ),
+ ];
+ });
+ }
+ await expect(page.getByTestId('hydration-selector')).toHaveText('Server:1');
+ await expect(page.getByLabel('Hydrated name')).toHaveValue('Server');
+
+ await page.evaluate(() => window.hydrationRuntime.gate.release());
+ for (const mode of [
+ 'selector',
+ 'tracked',
+ 'bound-selector',
+ 'bound-tracked',
+ ]) {
+ await expect(page.getByTestId(`hydration-${mode}`)).toHaveText('Saved:8');
+ }
+ await page.waitForFunction(
+ () => window.hydrationRuntime.probe.childHydrated,
+ );
+ expect(
+ await page.evaluate(() => ({
+ recoverableErrors: window.hydrationRuntime.probe.recoverableErrors,
+ firstValue: window.hydrationRuntime.probe.valuesRead[0],
+ elementCount: window.hydrationRuntime.probe.serverElements.length,
+ retainedElements: window.hydrationRuntime.probe.serverElements.every(
+ (element, index) =>
+ element ===
+ document.querySelectorAll(
+ '#hydration-root output, #hydration-root input',
+ )[index],
+ ),
+ })),
+ ).toEqual({
+ recoverableErrors: [],
+ firstValue: 'Server:1',
+ elementCount: 5,
+ retainedElements: true,
+ });
+
+ const input = page.getByLabel('Hydrated name');
+ await input.focus();
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 2),
+ );
+ await input.pressSequentially('XY');
+ await expect(input).toHaveValue('SaXYved');
+ for (const mode of [
+ 'selector',
+ 'tracked',
+ 'bound-selector',
+ 'bound-tracked',
+ ]) {
+ await expect(page.getByTestId(`hydration-${mode}`)).toHaveText(
+ 'SaXYved:10',
+ );
+ }
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(4);
+ });
+}
diff --git a/packages/classy-store/tests/browser/hydration/client.tsx b/packages/classy-store/tests/browser/hydration/client.tsx
new file mode 100644
index 0000000..29aa318
--- /dev/null
+++ b/packages/classy-store/tests/browser/hydration/client.tsx
@@ -0,0 +1,18 @@
+import {hydrateRoot} from 'react-dom/client';
+import {HydrationApp} from './components/hydrationApp/HydrationApp';
+import {createHydrationRuntime, type HydrationData} from './hydration.store';
+
+const container = document.getElementById('hydration-root');
+const initialDataElement = document.getElementById('initial-data');
+if (!container || !initialDataElement?.textContent) {
+ throw new Error('Missing server hydration fixture');
+}
+const initialData: HydrationData = JSON.parse(initialDataElement.textContent);
+const runtime = createHydrationRuntime(initialData, true);
+runtime.probe.serverElements = [...container.querySelectorAll('output, input')];
+window.hydrationRuntime = runtime;
+hydrateRoot(container, , {
+ onRecoverableError(error) {
+ runtime.probe.recoverableErrors.push(String(error));
+ },
+});
diff --git a/packages/classy-store/tests/browser/hydration/components/delayedFields/DelayedFields.tsx b/packages/classy-store/tests/browser/hydration/components/delayedFields/DelayedFields.tsx
new file mode 100644
index 0000000..28d85ae
--- /dev/null
+++ b/packages/classy-store/tests/browser/hydration/components/delayedFields/DelayedFields.tsx
@@ -0,0 +1,10 @@
+import type {HydrationRuntime} from '../../hydration.store';
+import {HydrationFields} from '../hydrationFields/HydrationFields';
+
+type Props = {runtime: HydrationRuntime};
+
+export function DelayedFields({runtime}: Props) {
+ // Suspend before any store hook runs, as when a client chunk is still loading.
+ if (!runtime.gate.ready) throw runtime.gate.pending;
+ return ;
+}
diff --git a/packages/classy-store/tests/browser/hydration/components/hydrationApp/HydrationApp.tsx b/packages/classy-store/tests/browser/hydration/components/hydrationApp/HydrationApp.tsx
new file mode 100644
index 0000000..65d088f
--- /dev/null
+++ b/packages/classy-store/tests/browser/hydration/components/hydrationApp/HydrationApp.tsx
@@ -0,0 +1,33 @@
+import {Suspense, useEffect} from 'react';
+import {persist} from '../../../../../src/utils/persist/persist';
+import {
+ type HydrationRuntime,
+ hydrationStorageKey,
+} from '../../hydration.store';
+import {DelayedFields} from '../delayedFields/DelayedFields';
+
+type Props = {runtime: HydrationRuntime};
+
+export function HydrationApp({runtime}: Props) {
+ useEffect(() => {
+ runtime.probe.parentHydrated = true;
+ const persistence = persist(runtime.store, {
+ name: hydrationStorageKey,
+ storage: localStorage,
+ skipHydration: true,
+ });
+ void persistence.rehydrate().then(() => {
+ runtime.probe.restored = true;
+ });
+ return persistence.unsubscribe;
+ }, [runtime]);
+
+ return (
+
+ Delayed store hydration
+ Loading fields }>
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/hydration/components/hydrationFields/HydrationFields.tsx b/packages/classy-store/tests/browser/hydration/components/hydrationFields/HydrationFields.tsx
new file mode 100644
index 0000000..e088424
--- /dev/null
+++ b/packages/classy-store/tests/browser/hydration/components/hydrationFields/HydrationFields.tsx
@@ -0,0 +1,41 @@
+import {useEffect} from 'react';
+import {useClassyStore} from '../../../../../src/frameworks/react/react';
+import type {HydrationRuntime} from '../../hydration.store';
+
+type Props = {runtime: HydrationRuntime};
+
+export function HydrationFields({runtime}: Props) {
+ const {store, useBoundStore} = runtime;
+ const selected = useClassyStore(
+ store,
+ (state) => `${state.user.name}:${state.count}`,
+ );
+ const tracked = useClassyStore(store);
+ const boundSelected = useBoundStore(
+ (state) => `${state.user.name}:${state.count}`,
+ );
+ const boundTracked = useBoundStore();
+ runtime.probe.valuesRead.push(selected);
+ useEffect(() => {
+ runtime.probe.childHydrated = true;
+ }, [runtime]);
+
+ return (
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/hydration/hydration.store.ts b/packages/classy-store/tests/browser/hydration/hydration.store.ts
new file mode 100644
index 0000000..e02d1b9
--- /dev/null
+++ b/packages/classy-store/tests/browser/hydration/hydration.store.ts
@@ -0,0 +1,50 @@
+import {createClassyStore} from '../../../src/core/core';
+import {
+ createStoreHook,
+ prepareStoreForHydration,
+} from '../../../src/frameworks/react/react';
+
+export type HydrationData = {user: {name: string}; count: number};
+
+export const hydrationStorageKey = 'classy-store-delayed-hydration';
+
+export function createHydrationRuntime(
+ initialData: HydrationData,
+ delayed: boolean,
+) {
+ const store = prepareStoreForHydration(createClassyStore(initialData));
+ let resolveBoundary = () => {};
+ const pending = new Promise((resolve) => {
+ resolveBoundary = resolve;
+ });
+ const gate = {
+ ready: !delayed,
+ pending,
+ release() {
+ gate.ready = true;
+ resolveBoundary();
+ },
+ };
+
+ return {
+ store,
+ useBoundStore: createStoreHook(store),
+ gate,
+ probe: {
+ parentHydrated: false,
+ restored: false,
+ childHydrated: false,
+ valuesRead: [] as string[],
+ recoverableErrors: [] as string[],
+ serverElements: [] as Element[],
+ },
+ };
+}
+
+export type HydrationRuntime = ReturnType;
+
+declare global {
+ interface Window {
+ hydrationRuntime: HydrationRuntime;
+ }
+}
diff --git a/packages/classy-store/tests/browser/hydration/renderHydrationPage.ts b/packages/classy-store/tests/browser/hydration/renderHydrationPage.ts
new file mode 100644
index 0000000..8dd052a
--- /dev/null
+++ b/packages/classy-store/tests/browser/hydration/renderHydrationPage.ts
@@ -0,0 +1,75 @@
+import {createElement} from 'react';
+import {renderToReadableStream, renderToString} from 'react-dom/server';
+import {HydrationApp} from './components/hydrationApp/HydrationApp';
+import {createHydrationRuntime, type HydrationRuntime} from './hydration.store';
+
+const pendingStreams = new Map();
+
+export function renderHydrationPage(request: Request): Response {
+ const query = new URL(request.url).searchParams;
+ const initialData = {user: {name: query.get('name') ?? 'Server'}, count: 1};
+ const runtime = createHydrationRuntime(initialData, false);
+ const markup = renderToString(createElement(HydrationApp, {runtime}));
+ const serializedData = JSON.stringify(initialData).replaceAll('<', '\\u003c');
+ return new Response(
+ `Delayed hydration ${markup}
`,
+ {
+ headers: {'Content-Type': 'text/html; charset=utf-8'},
+ },
+ );
+}
+
+export async function renderStreamingHydrationPage(
+ request: Request,
+): Promise {
+ const query = new URL(request.url).searchParams;
+ const requestId = query.get('id');
+ if (!requestId) return new Response('Missing stream id', {status: 400});
+ const initialData = {user: {name: 'Server'}, count: 1};
+ const runtime = createHydrationRuntime(initialData, true);
+ pendingStreams.set(requestId, runtime);
+ request.signal.addEventListener('abort', runtime.gate.release, {once: true});
+ const reactStream = await renderToReadableStream(
+ createElement(HydrationApp, {runtime}),
+ );
+ const encoder = new TextEncoder();
+ // WebKit buffers very small HTML responses before parsing streamed scripts.
+ const shellPadding = ``;
+ const responseStream = new ReadableStream({
+ async start(controller) {
+ controller.enqueue(
+ encoder.encode(
+ `Streaming hydration ${shellPadding}`,
+ ),
+ );
+ const reader = reactStream.getReader();
+ try {
+ while (true) {
+ const chunk = await reader.read();
+ if (chunk.done) break;
+ controller.enqueue(chunk.value);
+ }
+ controller.enqueue(encoder.encode('
'));
+ controller.close();
+ } catch (error) {
+ controller.error(error);
+ } finally {
+ pendingStreams.delete(requestId);
+ request.signal.removeEventListener('abort', runtime.gate.release);
+ reader.releaseLock();
+ }
+ },
+ });
+ return new Response(responseStream, {
+ headers: {'Content-Type': 'text/html; charset=utf-8'},
+ });
+}
+
+export function releaseStreamingBoundary(request: Request): Response {
+ const requestId = new URL(request.url).searchParams.get('id');
+ const runtime = requestId ? pendingStreams.get(requestId) : undefined;
+ if (request.method !== 'POST' || !runtime)
+ return new Response('Unknown stream', {status: 404});
+ runtime.gate.release();
+ return new Response('Released');
+}
diff --git a/packages/classy-store/tests/browser/index.html b/packages/classy-store/tests/browser/index.html
new file mode 100644
index 0000000..9eab491
--- /dev/null
+++ b/packages/classy-store/tests/browser/index.html
@@ -0,0 +1,8 @@
+
+
+ React input regressions
+
+
+
+
+
diff --git a/packages/classy-store/tests/browser/inputs.spec.ts b/packages/classy-store/tests/browser/inputs.spec.ts
new file mode 100644
index 0000000..3cbed41
--- /dev/null
+++ b/packages/classy-store/tests/browser/inputs.spec.ts
@@ -0,0 +1,125 @@
+import {expect} from '@playwright/test';
+import type {browserProbe} from './browser.store';
+import {test} from './browser.test';
+
+const modes = ['selector', 'tracked', 'bound-selector', 'bound-tracked'];
+
+for (const mode of modes) {
+ test.describe(mode, () => {
+ test.beforeEach(async ({page}) => {
+ await page.goto(`/?mode=${mode}`);
+ await expect(page.getByLabel('Text', {exact: true})).toHaveValue(
+ 'abcdef',
+ );
+ });
+
+ test('keeps the caret during middle insertion and selection replacement', async ({
+ page,
+ }) => {
+ const input = page.getByLabel('Text', {exact: true});
+ await input.focus();
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 2),
+ );
+ await input.pressSequentially('XY');
+ await expect(input).toHaveValue('abXYcdef');
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(4);
+ await expect(page.getByTestId('text')).toHaveText('abXYcdef');
+ await expect(page.getByTestId('edits')).toHaveText('2');
+
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 4),
+ );
+ await input.pressSequentially('Z');
+ await expect(input).toHaveValue('abZcdef');
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(3);
+ await input.press('Backspace');
+ await expect(input).toHaveValue('abcdef');
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(2);
+ });
+
+ test('handles multiline input, unicode insertion, and checkbox changes', async ({
+ page,
+ }) => {
+ const notes = page.getByLabel('Notes');
+ await notes.focus();
+ await notes.evaluate((element: HTMLTextAreaElement) =>
+ element.setSelectionRange(5, 5),
+ );
+ await notes.pressSequentially('new');
+ await notes.press('Enter');
+ await page.keyboard.insertText('ζ₯ζ¬θͺ');
+ await expect(notes).toHaveValue('firstnew\nζ₯ζ¬θͺ line');
+ await expect(page.getByTestId('notes')).toHaveText(
+ 'firstnew\nζ₯ζ¬θͺ line',
+ );
+ expect(
+ await notes.evaluate(
+ (element: HTMLTextAreaElement) => element.selectionStart,
+ ),
+ ).toBe(12);
+ await page.getByLabel('Enabled').check();
+ await expect(page.getByTestId('enabled')).toHaveText('true');
+ await page.getByLabel('Enabled').uncheck();
+ await expect(page.getByTestId('enabled')).toHaveText('false');
+ });
+
+ test('preserves text through a composition event sequence', async ({
+ page,
+ }) => {
+ // Synthetic composition boundaries + native browser text insertion.
+ // OS-level IME candidate selection still requires a manual smoke test.
+ const input = page.getByLabel('Text', {exact: true});
+ await input.focus();
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 2),
+ );
+ await input.dispatchEvent('compositionstart', {data: ''});
+ await page.keyboard.insertText('γ«');
+ await input.dispatchEvent('compositionupdate', {data: 'γ«'});
+ await input.dispatchEvent('compositionend', {data: 'γ«'});
+ await expect(input).toHaveValue('abγ«cdef');
+ await expect(page.getByTestId('text')).toHaveText('abγ«cdef');
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(3);
+ // Firefox's insertText also emits its own composition boundaries.
+ const compositions = await page.evaluate(
+ () => window.browserProbe.compositions,
+ );
+ expect(compositions[0]).toBe('start');
+ expect(compositions.at(-1)).toBe('end');
+ expect(compositions.filter((event) => event === 'start').length).toBe(
+ compositions.filter((event) => event === 'end').length,
+ );
+ });
+
+ test('commits multi-property and array updates together while ordinary subscriptions batch', async ({
+ page,
+ }) => {
+ await page.getByRole('button', {name: 'Update together'}).click();
+ await expect(page.getByTestId('full-name')).toHaveText('Ada Lovelace');
+ await expect(page.getByTestId('items')).toHaveText('z,c,x');
+ const probe: typeof browserProbe = await page.evaluate(
+ () => window.browserProbe,
+ );
+ expect(probe.notificationsDuringEvent).toBe(0);
+ expect(probe.notifications).toBe(1);
+ expect(probe.commits).toEqual(['Ada Lovelace:z,c,x']);
+ });
+ });
+}
diff --git a/packages/classy-store/tests/browser/persistence.spec.ts b/packages/classy-store/tests/browser/persistence.spec.ts
new file mode 100644
index 0000000..25cbfca
--- /dev/null
+++ b/packages/classy-store/tests/browser/persistence.spec.ts
@@ -0,0 +1,94 @@
+import {expect, type Page} from '@playwright/test';
+import {test} from './browser.test';
+
+async function expectDraft(page: Page, text: string) {
+ await expect(page.getByLabel('Draft', {exact: true})).toHaveValue(text);
+ await expect(page.getByTestId('tracked')).toHaveText(text);
+ await expect(page.getByTestId('selected')).toHaveText(text);
+}
+
+test('two tabs synchronize real inputs without echo writes, restore on reload, and unsubscribe', async ({
+ page,
+ context,
+}) => {
+ const other = await context.newPage();
+ await page.goto('/persistence');
+ await other.goto('/persistence');
+ await expectDraft(page, 'Initial');
+ await expectDraft(other, 'Initial');
+
+ await page.getByLabel('Draft', {exact: true}).fill('From first tab');
+ await expectDraft(other, 'From first tab');
+ expect(await other.evaluate(() => window.persistenceProbe.writes)).toBe(0);
+ await other.getByLabel('Draft', {exact: true}).fill('From second tab');
+ await expectDraft(page, 'From second tab');
+ expect(await page.evaluate(() => window.persistenceProbe.writes)).toBe(1);
+ expect(await other.evaluate(() => window.persistenceProbe.writes)).toBe(1);
+ expect(
+ await other.evaluate(() => window.persistenceProbe.trustedEvents),
+ ).toBe(1);
+
+ await page.reload();
+ await expectDraft(page, 'From second tab');
+ await other.getByRole('button', {name: 'Stop persistence'}).click();
+ await expect(other.getByTestId('status')).toHaveText('Disconnected');
+ await page.getByLabel('Draft', {exact: true}).fill('After cleanup');
+ // Wait for actual delivery before asserting that the retired handle ignored it.
+ await expect
+ .poll(() => other.evaluate(() => window.persistenceProbe.events))
+ .toBe(2);
+ await expectDraft(other, 'From second tab');
+ await other.getByLabel('Draft', {exact: true}).fill('Local only');
+ await expectDraft(other, 'Local only');
+ expect(await other.evaluate(() => window.persistenceProbe.writes)).toBe(1);
+ expect(
+ await page.evaluate(
+ () => JSON.parse(localStorage.getItem('two-tab-draft') ?? '').state.text,
+ ),
+ ).toBe('After cleanup');
+});
+
+test('a remote update cancels the other tabβs pending debounce and local editing can resume', async ({
+ page,
+ context,
+}) => {
+ const other = await context.newPage();
+ await page.goto('/persistence?debounce=500');
+ await other.goto('/persistence');
+ await expectDraft(page, 'Initial');
+ await expectDraft(other, 'Initial');
+ await page.getByLabel('Draft', {exact: true}).fill('Pending local edit');
+ await other.getByLabel('Draft', {exact: true}).fill('Remote winner');
+ await expectDraft(page, 'Remote winner');
+ // A negative timer assertion must observe the original debounce deadline.
+ await page.waitForTimeout(650);
+ expect(await page.evaluate(() => window.persistenceProbe.writes)).toBe(0);
+ expect(await other.evaluate(() => window.persistenceProbe.writes)).toBe(1);
+ await page.getByLabel('Draft', {exact: true}).fill('Editing resumed');
+ await expectDraft(other, 'Editing resumed');
+ expect(await page.evaluate(() => window.persistenceProbe.writes)).toBe(1);
+});
+
+test('an older storage event cannot overwrite a newer local save', async ({
+ page,
+ context,
+}) => {
+ const other = await context.newPage();
+ await page.goto('/persistence');
+ await other.goto('/persistence');
+ await expectDraft(other, 'Initial');
+ await other.evaluate(() =>
+ window.persistenceProbe.writeOnNextEvent('Newer local save'),
+ );
+ await page.getByLabel('Draft', {exact: true}).fill('Older remote save');
+ await expectDraft(page, 'Newer local save');
+ await expectDraft(other, 'Newer local save');
+ expect(
+ await other.evaluate(() => window.persistenceProbe.trustedEvents),
+ ).toBe(1);
+ expect(
+ await other.evaluate(
+ () => JSON.parse(localStorage.getItem('two-tab-draft') ?? '').state.text,
+ ),
+ ).toBe('Newer local save');
+});
diff --git a/packages/classy-store/tests/browser/persistence/client.tsx b/packages/classy-store/tests/browser/persistence/client.tsx
new file mode 100644
index 0000000..9a221a8
--- /dev/null
+++ b/packages/classy-store/tests/browser/persistence/client.tsx
@@ -0,0 +1,8 @@
+import {createRoot} from 'react-dom/client';
+import {PersistenceForm} from './components/persistenceForm/PersistenceForm';
+import {persistence} from './persistence.store';
+
+await persistence.hydrated;
+const container = document.getElementById('root');
+if (!container) throw new Error('Missing persistence fixture root');
+createRoot(container).render( );
diff --git a/packages/classy-store/tests/browser/persistence/components/persistenceForm/PersistenceForm.tsx b/packages/classy-store/tests/browser/persistence/components/persistenceForm/PersistenceForm.tsx
new file mode 100644
index 0000000..9a4636e
--- /dev/null
+++ b/packages/classy-store/tests/browser/persistence/components/persistenceForm/PersistenceForm.tsx
@@ -0,0 +1,36 @@
+import {useState} from 'react';
+import {useClassyStore} from '../../../../../src/frameworks/react/react';
+import {persistence, persistenceStore} from '../../persistence.store';
+
+export function PersistenceForm() {
+ const tracked = useClassyStore(persistenceStore);
+ const selected = useClassyStore(persistenceStore, (state) => state.text);
+ const [connected, setConnected] = useState(true);
+ return (
+
+
+ Draft
+ {
+ persistenceStore.text = event.target.value;
+ }}
+ />
+
+ {tracked.text}
+ {selected}
+ {
+ persistence.unsubscribe();
+ setConnected(false);
+ }}
+ >
+ Stop persistence
+
+
+ {connected ? 'Connected' : 'Disconnected'}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/browser/persistence/index.html b/packages/classy-store/tests/browser/persistence/index.html
new file mode 100644
index 0000000..8b9ebc7
--- /dev/null
+++ b/packages/classy-store/tests/browser/persistence/index.html
@@ -0,0 +1,8 @@
+
+
+ Persistence across tabs
+
+
+
+
+
diff --git a/packages/classy-store/tests/browser/persistence/persistence.store.ts b/packages/classy-store/tests/browser/persistence/persistence.store.ts
new file mode 100644
index 0000000..d28fce2
--- /dev/null
+++ b/packages/classy-store/tests/browser/persistence/persistence.store.ts
@@ -0,0 +1,54 @@
+import {createClassyStore} from '../../../src/index';
+import {persist} from '../../../src/utils/index';
+
+const parameters = new URLSearchParams(location.search);
+export const persistenceKey = parameters.get('key') ?? 'two-tab-draft';
+export const persistenceStore = createClassyStore({text: 'Initial'});
+let nextEventEdit: string | undefined;
+
+export const persistenceProbe = {
+ writes: 0,
+ events: 0,
+ trustedEvents: 0,
+ writeOnNextEvent(text: string) {
+ nextEventEdit = text;
+ },
+};
+
+// A capture listener makes the conflict deterministic: a newer local save
+// happens while an older, genuinely browser-delivered event is being handled.
+window.addEventListener(
+ 'storage',
+ (event) => {
+ if (event.key !== persistenceKey || event.newValue === null) return;
+ persistenceProbe.events++;
+ if (event.isTrusted) persistenceProbe.trustedEvents++;
+ if (nextEventEdit !== undefined) {
+ persistenceStore.text = nextEventEdit;
+ nextEventEdit = undefined;
+ void persistence.save();
+ }
+ },
+ true,
+);
+
+export const persistence = persist(persistenceStore, {
+ name: persistenceKey,
+ storage: {
+ getItem: (name) => localStorage.getItem(name),
+ setItem: (name, value) => {
+ persistenceProbe.writes++;
+ localStorage.setItem(name, value);
+ },
+ removeItem: (name) => localStorage.removeItem(name),
+ },
+ debounce: Number(parameters.get('debounce') ?? 0),
+ syncTabs: true,
+});
+
+declare global {
+ interface Window {
+ persistenceProbe: typeof persistenceProbe;
+ }
+}
+window.persistenceProbe = persistenceProbe;
diff --git a/packages/classy-store/tests/browser/playwright.config.ts b/packages/classy-store/tests/browser/playwright.config.ts
new file mode 100644
index 0000000..c2eff5b
--- /dev/null
+++ b/packages/classy-store/tests/browser/playwright.config.ts
@@ -0,0 +1,24 @@
+import {fileURLToPath} from 'node:url';
+import {defineConfig, devices} from '@playwright/test';
+
+export default defineConfig({
+ testDir: '.',
+ testMatch: '*.spec.ts',
+ fullyParallel: true,
+ workers: 3,
+ forbidOnly: Boolean(process.env.CI),
+ retries: 0,
+ reporter: 'list',
+ use: {baseURL: 'http://127.0.0.1:4179', trace: 'retain-on-failure'},
+ projects: [
+ {name: 'chromium', use: {...devices['Desktop Chrome']}},
+ {name: 'firefox', use: {...devices['Desktop Firefox']}},
+ {name: 'webkit', use: {...devices['Desktop Safari']}},
+ ],
+ webServer: {
+ command: 'bun run tests/browser/server.ts',
+ cwd: fileURLToPath(new URL('../..', import.meta.url)),
+ url: 'http://127.0.0.1:4179',
+ reuseExistingServer: false,
+ },
+});
diff --git a/packages/classy-store/tests/browser/qa.spec.ts b/packages/classy-store/tests/browser/qa.spec.ts
new file mode 100644
index 0000000..ba080ff
--- /dev/null
+++ b/packages/classy-store/tests/browser/qa.spec.ts
@@ -0,0 +1,53 @@
+import {expect} from '@playwright/test';
+import {test} from './browser.test';
+
+const modes = ['selector', 'tracked', 'bound-selector', 'bound-tracked'];
+
+test('recovers derived selectors from intermediate multi-write states without errors', async ({
+ page,
+}) => {
+ await page.goto('/?mode=derived');
+ await expect(page.getByTestId('derived-values')).toHaveText(
+ 'Ada|Ada|Ada,Grace,Linus|Ada,Grace,Linus',
+ );
+ await page.getByRole('button', {name: 'Replace selection'}).click();
+ await expect(page.getByTestId('derived-values')).toHaveText(
+ 'Hedy|Hedy|Katherine,Linus,Hedy|Katherine,Linus,Hedy',
+ );
+ await expect(page.locator('#notifications-during-event')).toHaveText('0');
+ expect(await page.getByTestId('derived-commits').textContent()).toBe(
+ 'Ada|Ada|Ada,Grace,Linus|Ada,Grace,Linus\nHedy|Hedy|Katherine,Linus,Hedy|Katherine,Linus,Hedy\n',
+ );
+});
+
+for (const mode of modes) {
+ test(`${mode}: native clipboard paste replaces selection and preserves caret`, async ({
+ page,
+ context,
+ browserName,
+ }) => {
+ test.skip(
+ browserName !== 'chromium',
+ 'Clipboard permissions are Chromium-specific',
+ );
+ await context.grantPermissions(['clipboard-read', 'clipboard-write']);
+ await page.goto(`/?mode=${mode}`);
+ const input = page.getByLabel('Text', {exact: true});
+ await expect(input).toHaveValue('abcdef');
+ await page.evaluate(() => navigator.clipboard.writeText('pasted ζ₯ζ¬θͺ'));
+ await input.focus();
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 4),
+ );
+ await input.press('ControlOrMeta+V');
+ await expect(input).toHaveValue('abpasted ζ₯ζ¬θͺef');
+ await expect(page.getByTestId('text')).toHaveText('abpasted ζ₯ζ¬θͺef');
+ expect(
+ await input.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(12);
+ await input.pressSequentially('!');
+ await expect(input).toHaveValue('abpasted ζ₯ζ¬θͺ!ef');
+ });
+}
diff --git a/packages/classy-store/tests/browser/server.ts b/packages/classy-store/tests/browser/server.ts
new file mode 100644
index 0000000..2cec8b7
--- /dev/null
+++ b/packages/classy-store/tests/browser/server.ts
@@ -0,0 +1,87 @@
+import {fileURLToPath} from 'node:url';
+import {compileConsumer} from '../compiler/compileConsumer.mjs';
+import {
+ releaseStreamingBoundary,
+ renderHydrationPage,
+ renderStreamingHydrationPage,
+} from './hydration/renderHydrationPage';
+import page from './index.html';
+import persistencePage from './persistence/index.html';
+
+const hydrationBundle = await Bun.build({
+ entrypoints: [
+ fileURLToPath(new URL('./hydration/client.tsx', import.meta.url)),
+ ],
+ target: 'browser',
+ format: 'iife',
+ define: {'process.env.NODE_ENV': '"development"'},
+});
+if (!hydrationBundle.success)
+ throw new Error('Could not bundle hydration fixture');
+const hydrationScript = await hydrationBundle.outputs[0].text();
+
+const compilerBundle = await Bun.build({
+ entrypoints: [
+ fileURLToPath(new URL('./compiler/client.tsx', import.meta.url)),
+ ],
+ target: 'browser',
+ format: 'iife',
+ define: {'process.env.NODE_ENV': '"development"'},
+ plugins: [
+ {
+ name: 'react-compiler-consumer',
+ setup(build) {
+ build.onLoad(
+ {filter: /\/compiler\/components\/.*\.tsx$/},
+ async ({path}) => {
+ const componentName = path
+ .split('/')
+ .at(-1)
+ ?.replace(/\.tsx$/, '');
+ if (!componentName)
+ throw new Error('Missing compiler component name');
+ const {code} = await compileConsumer(
+ await Bun.file(path).text(),
+ path,
+ '19',
+ [componentName],
+ );
+ return {contents: code, loader: 'tsx'};
+ },
+ );
+ },
+ },
+ ],
+});
+if (!compilerBundle.success)
+ throw new AggregateError(
+ compilerBundle.logs,
+ 'Could not bundle compiled consumer fixture',
+ );
+const compilerScript = await compilerBundle.outputs[0].text();
+
+Bun.serve({
+ hostname: '127.0.0.1',
+ port: 4179,
+ routes: {
+ '/': page,
+ '/persistence': persistencePage,
+ '/compiler': () =>
+ new Response(
+ 'Compiled consumer
',
+ {headers: {'Content-Type': 'text/html; charset=utf-8'}},
+ ),
+ '/compiler/client.js': () =>
+ new Response(compilerScript, {
+ headers: {'Content-Type': 'text/javascript; charset=utf-8'},
+ }),
+ '/hydration': renderHydrationPage,
+ '/hydration/stream': renderStreamingHydrationPage,
+ '/hydration/release': releaseStreamingBoundary,
+ '/hydration/client.js': () =>
+ new Response(hydrationScript, {
+ headers: {'Content-Type': 'text/javascript; charset=utf-8'},
+ }),
+ },
+ development: false,
+});
diff --git a/packages/classy-store/tests/browser/tsconfig.json b/packages/classy-store/tests/browser/tsconfig.json
new file mode 100644
index 0000000..7afd278
--- /dev/null
+++ b/packages/classy-store/tests/browser/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "../.."
+ },
+ "include": ["**/*.ts", "**/*.tsx"]
+}
diff --git a/packages/classy-store/tests/compatibility/README.md b/packages/classy-store/tests/compatibility/README.md
new file mode 100644
index 0000000..0b42ae1
--- /dev/null
+++ b/packages/classy-store/tests/compatibility/README.md
@@ -0,0 +1,27 @@
+# React package compatibility checks
+
+From the repository root, run:
+
+```sh
+bun run build
+node packages/classy-store/tests/compatibility/runCompatibility.mjs
+```
+
+The runner packs the existing build, installs that tarball into temporary consumers, and checks React 18.0.0, 18.3.1, and the version pinned by the library. It requires Node, npm, Bun, and registry access. To check selected versions, pass exact matching `react` / `react-dom` versions:
+
+```sh
+node packages/classy-store/tests/compatibility/runCompatibility.mjs 18.0.0 19.3.1
+```
+
+`BUN_EXECUTABLE` can select a different Bun executable. `KEEP_COMPATIBILITY_TEMP=1` keeps the temporary consumers for debugging; otherwise they are removed, including after failures. The library workspace and lockfile are not changed. The runner deliberately does not rebuild, so build first after source edits.
+
+Each consumer verifies:
+
+- Node ESM and CommonJS exports, including server rendering without a browser global, shared state across the core, React, and utilities entry points, and an idempotent prepared hydration snapshot that remains stable after live mutations.
+- Strict Mode mounting, selector changes, store switching, synchronous multi-property React updates, and ordinary batched subscriptions against the installed package.
+- The source SSR/hydration regression suites against the installed package, including DOM reuse, independent request state, and persistence updates before a Suspense boundary's first store read. Direct and bound hooks must hydrate with the prepared baseline and then commit the persisted value. Source-only internal listener counts are omitted; public unmount and persistence cleanup assertions remain.
+- TypeScript NodeNext resolution through both `.mts` and `.cts` consumers: selector inference, nested readonly snapshots, mutable stores and `prepareStoreForHydration` return types, and compile-time rejection of React `sync` options.
+- The real scoped editor context example, copied directly from `examples/react/src/demos/scoped-store/store` into each consumer. Runtime coverage checks independent instances, nested providers, stable ownership, keyed resets, persistence cleanup, request isolation, and matching hydration. Delayed Suspense tests prove the provider can restore persistence before a child's first store read without replacing server DOM, with both selector and auto-tracked consumers.
+- Strict TypeScript checks for the copied context/provider example and its scoping fixtures, including typed mutable actions and immutable snapshot reads. The example uses the installed package exclusively; no source imports or additional public helpers are substituted.
+
+Tests intentionally use the React 18 `react-dom/test-utils` fallback when `React.act` is unavailable. React is external to the published build, so each consumer uses only its selected React version.
diff --git a/packages/classy-store/tests/compatibility/consumer.fixture.tsx b/packages/classy-store/tests/compatibility/consumer.fixture.tsx
new file mode 100644
index 0000000..db57746
--- /dev/null
+++ b/packages/classy-store/tests/compatibility/consumer.fixture.tsx
@@ -0,0 +1,151 @@
+import {afterEach, describe, expect, test} from 'bun:test';
+import {createClassyStore, subscribe} from '@codebelt/classy-store';
+import {createStoreHook, useClassyStore} from '@codebelt/classy-store/react';
+import {StrictMode, useLayoutEffect, useMemo} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {act} from './reactAct';
+
+class ConsumerStore {
+ count = 0;
+ firstName = 'Ada';
+ lastName = 'Lovelace';
+ names = ['Alice', 'Bob'];
+ unrelated = 0;
+
+ updateProfile() {
+ this.firstName = 'Grace';
+ this.lastName = 'Hopper';
+ this.count++;
+ }
+}
+
+type Selection = {name: string};
+type ConsumerViewProps = {
+ store: ConsumerStore;
+ index?: number;
+ isEqual?: (previous: Selection, next: Selection) => boolean;
+ onCommit?: (value: string) => void;
+};
+
+function ConsumerView({
+ store,
+ index = 0,
+ isEqual,
+ onCommit,
+}: ConsumerViewProps) {
+ const tracked = useClassyStore(store);
+ const selected = useClassyStore(store, (state) => state.names[index]);
+ const useStore = useMemo(() => createStoreHook(store), [store]);
+ const bound = useStore((state) => ({name: state.names[index]}), {
+ isEqual: isEqual ?? ((previous, next) => previous.name === next.name),
+ });
+ const profile = useClassyStore(
+ store,
+ (state) => `${state.firstName} ${state.lastName}`,
+ );
+ const value = `${tracked.count}|${profile}|${selected}|${bound.name}`;
+ useLayoutEffect(() => {
+ onCommit?.(value);
+ });
+ return {value} ;
+}
+
+const mounted: {root: Root; container: HTMLDivElement}[] = [];
+
+afterEach(async () => {
+ for (const {root, container} of mounted.splice(0)) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+});
+
+async function mount(props: ConsumerViewProps) {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mounted.push({root, container});
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ return {root, container};
+}
+
+describe('packed React consumer', () => {
+ test('commits a multi-property update synchronously while normal subscribers batch', async () => {
+ const store = createClassyStore(new ConsumerStore());
+ const commits: string[] = [];
+ const {container} = await mount({
+ store,
+ onCommit: (value) => commits.push(value),
+ });
+ commits.length = 0;
+ const notifications: string[] = [];
+ const unsubscribe = subscribe(store, () => {
+ notifications.push(`${store.firstName} ${store.lastName}`);
+ });
+
+ try {
+ act(() => {
+ store.updateProfile();
+ expect(notifications).toEqual([]);
+ });
+ expect(container.textContent).toBe('1|Grace Hopper|Alice|Alice');
+ expect(commits).toEqual(['1|Grace Hopper|Alice|Alice']);
+
+ await Promise.resolve();
+ expect(notifications).toEqual(['Grace Hopper']);
+ commits.length = 0;
+ await act(async () => {
+ store.unrelated++;
+ });
+ expect(commits).toEqual([]);
+ } finally {
+ unsubscribe();
+ }
+ });
+
+ test('updates selectors when props and equality functions change', async () => {
+ const store = createClassyStore(new ConsumerStore());
+ const {root, container} = await mount({store});
+ act(() => root.render( ));
+ expect(container.textContent).toBe('0|Ada Lovelace|Bob|Bob');
+
+ const ignoreChanges = () => true;
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+ act(() => {
+ store.names[1] = 'Bobby';
+ });
+ expect(container.textContent).toBe('0|Ada Lovelace|Bobby|Bob');
+ act(() => root.render( ));
+ expect(container.textContent).toBe('0|Ada Lovelace|Bobby|Bobby');
+ });
+
+ test('switches stores and releases the old subscription', async () => {
+ const firstStore = createClassyStore(new ConsumerStore());
+ const secondStore = createClassyStore(new ConsumerStore());
+ secondStore.count = 7;
+ const commits: string[] = [];
+ const onCommit = (value: string) => commits.push(value);
+ const {root, container} = await mount({store: firstStore, onCommit});
+ act(() => {
+ root.render( );
+ });
+ expect(container.textContent).toBe('7|Ada Lovelace|Alice|Alice');
+ commits.length = 0;
+
+ await act(async () => {
+ firstStore.updateProfile();
+ });
+ expect(commits).toEqual([]);
+ act(() => secondStore.updateProfile());
+ expect(container.textContent).toBe('8|Grace Hopper|Alice|Alice');
+ });
+});
diff --git a/packages/classy-store/tests/compatibility/consumer.types.fixture.ts b/packages/classy-store/tests/compatibility/consumer.types.fixture.ts
new file mode 100644
index 0000000..2fc3cc7
--- /dev/null
+++ b/packages/classy-store/tests/compatibility/consumer.types.fixture.ts
@@ -0,0 +1,160 @@
+import {
+ createClassyStore,
+ type Snapshot,
+ snapshot,
+ subscribe,
+} from '@codebelt/classy-store';
+import {
+ type ReactiveMap,
+ type ReactiveSet,
+ reactiveMap,
+ reactiveSet,
+} from '@codebelt/classy-store/collections';
+// @ts-expect-error Auto-tracked hooks no longer expose notification options.
+import type {UseClassyStoreOptions} from '@codebelt/classy-store/react';
+import {
+ createStoreHook,
+ prepareStoreForHydration,
+ useClassyStore,
+ useLocalStore,
+} from '@codebelt/classy-store/react';
+import {
+ type PersistHandle,
+ type PersistOptions,
+ persist,
+ type StorageAdapter,
+ subscribeKey,
+} from '@codebelt/classy-store/utils';
+
+class ConsumerStore {
+ count = 0;
+ user = {name: 'Ada'};
+ names = ['Ada', 'Grace'];
+
+ increment() {
+ this.count++;
+ }
+}
+
+type Equal =
+ (() => Value extends Left ? 1 : 2) extends <
+ Value,
+ >() => Value extends Right ? 1 : 2
+ ? true
+ : false;
+type Expect = Condition;
+
+const store = createClassyStore(new ConsumerStore());
+const prepared = prepareStoreForHydration(store);
+const useStore = createStoreHook(prepared);
+
+export function useConsumerTypeAssertions() {
+ const count = useClassyStore(store, (state) => state.count);
+ const tracked = useClassyStore(store);
+ const boundCount = useStore((state) => state.count);
+ const boundTracked = useStore();
+ const selected = useClassyStore(
+ store,
+ (state) => ({label: state.user.name}),
+ {isEqual: (previous, next) => previous.label === next.label},
+ );
+ const boundSelected = useStore((state) => ({label: state.user.name}), {
+ isEqual: (previous, next) => previous.label === next.label,
+ });
+ const local = useLocalStore(() => new ConsumerStore());
+
+ const assertions: [
+ Expect>,
+ Expect>,
+ Expect>>,
+ Expect>>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ ] = [true, true, true, true, true, true, true, true];
+
+ // @ts-expect-error Snapshots cannot be assigned to.
+ tracked.count = 1;
+ // @ts-expect-error Nested snapshot objects remain readonly.
+ tracked.user.name = 'Changed';
+ // @ts-expect-error Snapshot arrays are readonly.
+ tracked.names.push('Changed');
+ // @ts-expect-error Bound snapshots are deeply readonly too.
+ boundTracked.user.name = 'Changed';
+ // @ts-expect-error Selector arguments are deeply readonly.
+ useClassyStore(store, (state) => state.names.push('Changed'));
+ // @ts-expect-error React automatically subscribes synchronously.
+ useClassyStore(store, {sync: true});
+ // @ts-expect-error React cannot opt back into batched subscriptions.
+ useClassyStore(store, {sync: false});
+ // @ts-expect-error Selector options only customize equality.
+ useClassyStore(store, (state) => state.count, {sync: true});
+ // @ts-expect-error Selector options cannot disable synchronous subscriptions.
+ useClassyStore(store, (state) => state.count, {sync: false});
+ // @ts-expect-error Bound hooks have no sync option.
+ useStore({sync: true});
+ // @ts-expect-error Bound auto-tracked hooks cannot batch subscriptions.
+ useStore({sync: false});
+ // @ts-expect-error Bound selector options have no sync option.
+ useStore((state) => state.count, {sync: true});
+ // @ts-expect-error Bound selector options have no sync option.
+ useStore((state) => state.count, {sync: false});
+ // @ts-expect-error Selector result inference cannot become any.
+ const wrongType: string = count;
+
+ local.increment();
+ prepared.increment();
+ prepared.count = 2;
+ prepared.user.name = 'Prepared and mutable';
+ prepared.names.push('Prepared and mutable');
+ store.user.name = 'Mutable';
+ store.names.push('Mutable');
+ const frozen = snapshot(store);
+ // @ts-expect-error Core snapshots are also deeply readonly.
+ frozen.user.name = 'Changed';
+ subscribe(store, () => {}, {sync: true})();
+
+ return {assertions, wrongType, removedOptions: null as UseClassyStoreOptions};
+}
+
+// Compiled through the packed ESM and CommonJS declarations by the existing
+// compatibility runner; this function is a type fixture and is never invoked.
+export function utilityTypeAssertions(storage: StorageAdapter) {
+ const draft = createClassyStore({title: 'Draft', count: 0});
+ const options: PersistOptions = {
+ name: 'draft',
+ storage,
+ properties: [
+ 'title',
+ {
+ key: 'count',
+ serialize: (value) => {
+ const inferred: Expect> = true;
+ void inferred;
+ return String(value);
+ },
+ deserialize: (stored) => Number(stored),
+ },
+ ],
+ };
+ const persistence = persist(draft, options);
+ const unsubscribe = subscribeKey(draft, 'count', (value, previous) => {
+ const inferred: [
+ Expect>,
+ Expect>,
+ ] = [true, true];
+ return {inferred, value, previous};
+ });
+ const users = reactiveMap([['ada', {name: 'Ada'}]]);
+ const user = users.get('ada');
+ const tags = reactiveSet(['saved']);
+ const assertions: [
+ Expect>,
+ Expect void>>,
+ Expect>>,
+ Expect>,
+ Expect>>,
+ ] = [true, true, true, true, true];
+ return {assertions, persistence, unsubscribe};
+}
diff --git a/packages/classy-store/tests/compatibility/entrypoints.fixture.mjs b/packages/classy-store/tests/compatibility/entrypoints.fixture.mjs
new file mode 100644
index 0000000..d9ef4bd
--- /dev/null
+++ b/packages/classy-store/tests/compatibility/entrypoints.fixture.mjs
@@ -0,0 +1,75 @@
+import assert from 'node:assert/strict';
+import {createRequire} from 'node:module';
+
+const require = createRequire(import.meta.url);
+const mode = process.argv[2];
+const load =
+ mode === 'require' ? (name) => require(name) : (name) => import(name);
+const {createClassyStore, snapshot, subscribe} = await load(
+ '@codebelt/classy-store',
+);
+const {createStoreHook, prepareStoreForHydration, useClassyStore} = await load(
+ '@codebelt/classy-store/react',
+);
+const {withHistory} = await load('@codebelt/classy-store/utils');
+const React = await load('react');
+const {renderToString} = await load('react-dom/server');
+
+assert.equal(typeof window, 'undefined');
+const store = createClassyStore({count: 1, nested: {label: 'server'}});
+const useStore = createStoreHook(store);
+
+function ServerConsumer() {
+ const tracked = useClassyStore(store);
+ const count = useStore((state) => state.count);
+ return React.createElement('span', null, `${tracked.nested.label}:${count}`);
+}
+
+assert.equal(
+ renderToString(React.createElement(ServerConsumer)),
+ 'server:1 ',
+);
+let notifications = 0;
+const unsubscribe = subscribe(store, () => notifications++);
+store.count = 2;
+store.nested.label = 'updated';
+assert.equal(notifications, 0);
+await Promise.resolve();
+assert.equal(notifications, 1);
+assert.equal(snapshot(store).count, 2);
+assert.ok(Object.isFrozen(snapshot(store).nested));
+unsubscribe();
+
+const history = withHistory(store);
+store.nested.label = 'later';
+await Promise.resolve();
+history.undo();
+store.nested.label = 'editable';
+assert.equal(store.nested.label, 'editable');
+history.dispose();
+
+// Capture before the first hook read: later live writes must not change the
+// initial snapshot used by either React entry point during server rendering.
+const preparedStore = createClassyStore({
+ count: 4,
+ nested: {label: 'prepared'},
+});
+assert.equal(prepareStoreForHydration(preparedStore), preparedStore);
+const usePreparedStore = createStoreHook(preparedStore);
+preparedStore.count = 7;
+preparedStore.nested.label = 'live';
+assert.equal(prepareStoreForHydration(preparedStore), preparedStore);
+
+function PreparedServerConsumer() {
+ const label = useClassyStore(preparedStore, (state) => state.nested.label);
+ const tracked = usePreparedStore();
+ return React.createElement('span', null, `${label}:${tracked.count}`);
+}
+
+assert.equal(
+ renderToString(React.createElement(PreparedServerConsumer)),
+ 'prepared:4 ',
+);
+assert.equal(snapshot(preparedStore).count, 7);
+assert.equal(preparedStore.nested.label, 'live');
+console.log(`Node ${mode} exports and SSR passed with React ${React.version}`);
diff --git a/packages/classy-store/tests/compatibility/runCompatibility.mjs b/packages/classy-store/tests/compatibility/runCompatibility.mjs
new file mode 100644
index 0000000..090eb06
--- /dev/null
+++ b/packages/classy-store/tests/compatibility/runCompatibility.mjs
@@ -0,0 +1,247 @@
+import {spawnSync} from 'node:child_process';
+import {
+ cpSync,
+ mkdtempSync,
+ readdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs';
+import {tmpdir} from 'node:os';
+import {dirname, join, resolve} from 'node:path';
+import {fileURLToPath} from 'node:url';
+
+const fixturesDirectory = dirname(fileURLToPath(import.meta.url));
+const packageDirectory = resolve(fixturesDirectory, '../..');
+const scopingFixturesDirectory = resolve(fixturesDirectory, '../scoping');
+const scopedExampleDirectory = resolve(
+ packageDirectory,
+ '../../examples/react/src/demos/scoped-store/store',
+);
+const scopingTestFiles = readdirSync(scopingFixturesDirectory)
+ .filter((filename) => filename.endsWith('.test.tsx'))
+ .sort();
+const packageManifest = JSON.parse(
+ readFileSync(join(packageDirectory, 'package.json'), 'utf8'),
+);
+const requestedVersions = process.argv.slice(2);
+const reactVersions = requestedVersions.length
+ ? requestedVersions
+ : ['18.0.0', '18.3.1', packageManifest.devDependencies.react];
+const bunExecutable = process.env.BUN_EXECUTABLE ?? 'bun';
+const temporaryDirectory = mkdtempSync(join(tmpdir(), 'classy-store-react-'));
+
+function run(command, argumentsList, cwd, captureOutput = false) {
+ const result = spawnSync(command, argumentsList, {
+ cwd,
+ encoding: 'utf8',
+ stdio: captureOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
+ env: {
+ ...process.env,
+ NODE_ENV: 'development',
+ npm_config_cache: join(temporaryDirectory, 'npm-cache'),
+ },
+ });
+ if (result.error || result.status !== 0) {
+ throw new Error(
+ `${command} ${argumentsList.join(' ')} failed\n${result.stderr ?? ''}`,
+ {cause: result.error},
+ );
+ }
+ return result.stdout;
+}
+
+function rewritePackedImports(source) {
+ return source
+ .replace('import {act, ', "import {act} from './reactAct';\nimport {")
+ .replaceAll("'../../core/core'", "'@codebelt/classy-store'")
+ .replaceAll("'../../snapshot/snapshot'", "'@codebelt/classy-store'")
+ .replaceAll(
+ "'../../utils/persist/persist'",
+ "'@codebelt/classy-store/utils'",
+ )
+ .replaceAll("'./react'", "'@codebelt/classy-store/react'");
+}
+
+try {
+ const packed = JSON.parse(
+ run(
+ 'npm',
+ [
+ 'pack',
+ '--ignore-scripts',
+ '--json',
+ '--pack-destination',
+ temporaryDirectory,
+ ],
+ packageDirectory,
+ true,
+ ),
+ );
+ const packageArchive = join(temporaryDirectory, packed[0].filename);
+
+ for (const reactVersion of reactVersions) {
+ const consumerDirectory = join(temporaryDirectory, `react-${reactVersion}`);
+ cpSync(fixturesDirectory, consumerDirectory, {recursive: true});
+ // Exercise the actual application example against the installed package.
+ // Keeping this a copy, rather than a rewritten fixture, catches drift in
+ // provider ownership, context types, and effect cleanup.
+ cpSync(scopingFixturesDirectory, consumerDirectory, {recursive: true});
+ cpSync(scopedExampleDirectory, join(consumerDirectory, 'example'), {
+ recursive: true,
+ });
+ const reactTypesVersion = reactVersion.startsWith('18.')
+ ? '18.3.27'
+ : packageManifest.devDependencies['@types/react'];
+ const reactDomTypesVersion = reactVersion.startsWith('18.')
+ ? '18.3.7'
+ : packageManifest.devDependencies['@types/react-dom'];
+ writeFileSync(
+ join(consumerDirectory, 'package.json'),
+ JSON.stringify({
+ private: true,
+ type: 'module',
+ dependencies: {
+ '@codebelt/classy-store': `file:${packageArchive}`,
+ react: reactVersion,
+ 'react-dom': reactVersion,
+ '@types/react': reactTypesVersion,
+ '@types/react-dom': reactDomTypesVersion,
+ '@types/bun': packageManifest.devDependencies['@types/bun'],
+ '@happy-dom/global-registrator':
+ packageManifest.devDependencies['@happy-dom/global-registrator'],
+ typescript: packageManifest.devDependencies.typescript,
+ },
+ }),
+ );
+ writeFileSync(
+ join(consumerDirectory, 'happydom.ts'),
+ "import {GlobalRegistrator} from '@happy-dom/global-registrator';\nGlobalRegistrator.register();\nglobalThis.IS_REACT_ACT_ENVIRONMENT = true;\n",
+ );
+ writeFileSync(
+ join(consumerDirectory, 'reactAct.ts'),
+ "import * as React from 'react';\nimport {act as legacyAct} from 'react-dom/test-utils';\nexport const act = React.act ?? legacyAct;\n",
+ );
+ cpSync(
+ join(fixturesDirectory, 'consumer.fixture.tsx'),
+ join(consumerDirectory, 'consumer.test.tsx'),
+ );
+ const ssrTests = rewritePackedImports(
+ readFileSync(
+ join(packageDirectory, 'src/frameworks/react/react.ssr.test.tsx'),
+ 'utf8',
+ ),
+ );
+ writeFileSync(join(consumerDirectory, 'ssr.test.tsx'), ssrTests);
+ const sourceHydrationTests = readFileSync(
+ join(packageDirectory, 'src/frameworks/react/react.hydration.test.tsx'),
+ 'utf8',
+ );
+ // Internal listener counts remain covered in source tests. Installed-package
+ // tests retain the public unmount, no-more-commits, and storage cleanup checks.
+ const internalAssertions =
+ /^\s*expect\(getInternal\(clientStore\)\.(?:syncListeners|batchedListeners)\.size\)\.toBe\([01]\);\n/gm;
+ if ([...sourceHydrationTests.matchAll(internalAssertions)].length !== 4) {
+ throw new Error(
+ 'Expected four source-only hydration listener assertions',
+ );
+ }
+ const hydrationTests = rewritePackedImports(
+ sourceHydrationTests
+ .replace('{createClassyStore, getInternal}', '{createClassyStore}')
+ .replace(internalAssertions, ''),
+ );
+ writeFileSync(
+ join(consumerDirectory, 'hydration.test.tsx'),
+ hydrationTests,
+ );
+ cpSync(
+ join(packageDirectory, 'src/frameworks/react/testHelpers'),
+ join(consumerDirectory, 'testHelpers'),
+ {recursive: true},
+ );
+ for (const extension of ['mts', 'cts']) {
+ cpSync(
+ join(fixturesDirectory, 'consumer.types.fixture.ts'),
+ join(consumerDirectory, `consumer.${extension}`),
+ );
+ }
+
+ console.log(`\nChecking packed package with React ${reactVersion}`);
+ run(
+ 'npm',
+ [
+ 'install',
+ '--ignore-scripts',
+ '--no-audit',
+ '--no-fund',
+ '--package-lock=false',
+ ],
+ consumerDirectory,
+ );
+ for (const mode of ['import', 'require']) {
+ run(
+ process.execPath,
+ ['entrypoints.fixture.mjs', mode],
+ consumerDirectory,
+ );
+ }
+ run(
+ bunExecutable,
+ [
+ 'test',
+ '--preload',
+ './happydom.ts',
+ './consumer.test.tsx',
+ './ssr.test.tsx',
+ './hydration.test.tsx',
+ ...scopingTestFiles.map((filename) => `./${filename}`),
+ ],
+ consumerDirectory,
+ );
+ run(
+ join(consumerDirectory, 'node_modules/.bin/tsc'),
+ [
+ '--noEmit',
+ '--strict',
+ '--skipLibCheck',
+ '--target',
+ 'ES2022',
+ '--module',
+ 'NodeNext',
+ '--moduleResolution',
+ 'NodeNext',
+ 'consumer.mts',
+ 'consumer.cts',
+ ],
+ consumerDirectory,
+ );
+ run(
+ join(consumerDirectory, 'node_modules/.bin/tsc'),
+ [
+ '--noEmit',
+ '--strict',
+ '--skipLibCheck',
+ '--target',
+ 'ES2022',
+ '--jsx',
+ 'react-jsx',
+ '--module',
+ 'ESNext',
+ '--moduleResolution',
+ 'Bundler',
+ '--types',
+ 'bun',
+ 'scoping.types.fixture.tsx',
+ ...scopingTestFiles,
+ ],
+ consumerDirectory,
+ );
+ }
+} finally {
+ if (process.env.KEEP_COMPATIBILITY_TEMP === '1') {
+ console.log(`Kept compatibility consumers at ${temporaryDirectory}`);
+ } else {
+ rmSync(temporaryDirectory, {recursive: true, force: true});
+ }
+}
diff --git a/packages/classy-store/tests/compiler/README.md b/packages/classy-store/tests/compiler/README.md
new file mode 100644
index 0000000..46175e7
--- /dev/null
+++ b/packages/classy-store/tests/compiler/README.md
@@ -0,0 +1,54 @@
+# React Compiler consumers
+
+Run `bun run test:react:compiler` from the repository root. The runner builds on
+the package's existing `dist`, packs that public package, and installs isolated
+React 18.0, 18.3, and current React 19 consumers. To check one version after a
+library build:
+
+```sh
+node packages/classy-store/tests/compiler/runCompiler.mjs 19.3.1
+```
+
+Consumer components are transformed by the pinned `babel-plugin-react-compiler`
+using its default inference rules. React 18 uses compiler target `18` and the
+`react-compiler-runtime` shim; React 19 uses target `19` and React's runtime.
+The package itself remains the published, uncompiled build. No compiler-specific
+configuration or opt-out directives are applied to the store hooks.
+
+The compile helper rejects compiler bailouts and requires every named component
+to report `CompileSuccess` with memo slots and emit a memo-cache runtime import.
+It preserves TypeScript and JSX for Bun to transpile after the compiler pass.
+The generated `compilation.json` records the named components and memoization
+statistics for each installed consumer.
+
+The runtime suite covers direct and bound hooks, selector/equality changes,
+conditional nested reads, memoized children, and local-state updates followed by
+store mutations. The cached array derivation cases reproduce a stale-view bug:
+after a local render, the compiler can retain an array mapping while rerunning
+only an indexed read; replacing the hook's complete read set loses dependencies
+still used by the cached result.
+
+Independent QA fixtures under `fixtures/` are compiled by the same pipeline and
+run alongside the main regressions. Browser fixtures reuse `compileConsumer`
+for controlled inputs and actual browser scheduling.
+
+The scale regressions run against the same installed package both before and
+after the Compiler transform. They mount 256 controlled fields and 512 list rows
+with selectors and auto-tracking, then assert exact committed-render counts for
+relevant, unrelated, no-op, and multi-property updates. Parent/local renders
+exercise memoized dependency retention; removing and remounting rows verifies
+that later updates stay isolated. These tests use layout-effect commit counters,
+not render-time side effects or wall-clock thresholds.
+
+The lifecycle regressions also run before and after the Compiler transform.
+They repeat StrictMode mounts, keyed `useLocalStore` resets, store-prop swaps,
+collection replacements, and truncation/refill for both selectors and auto-tracking. Getter-read
+counters detect callbacks left on retired stores even when React would ignore
+the resulting update. Mutating retained rows from replaced collections must
+leave the live root's version and subscription counters unchanged. These are
+deterministic cleanup checks; browser heap comparisons separately test whether
+unreferenced objects are collected.
+
+Set `KEEP_COMPILER_TEMP=1` to retain the installed consumers, transformed source,
+and compilation records. Set `BUN_EXECUTABLE` when testing with a specific Bun
+installation. The matrix needs npm registry access for its isolated consumers.
diff --git a/packages/classy-store/tests/compiler/compileConsumer.d.mts b/packages/classy-store/tests/compiler/compileConsumer.d.mts
new file mode 100644
index 0000000..387b21a
--- /dev/null
+++ b/packages/classy-store/tests/compiler/compileConsumer.d.mts
@@ -0,0 +1,11 @@
+import type {LoggerEvent} from 'babel-plugin-react-compiler';
+
+export function compileConsumer(
+ source: string,
+ filename: string,
+ target?: '18' | '19',
+ expectedFunctions?: string[],
+): {
+ code: string;
+ compiledFunctions: Extract[];
+};
diff --git a/packages/classy-store/tests/compiler/compileConsumer.mjs b/packages/classy-store/tests/compiler/compileConsumer.mjs
new file mode 100644
index 0000000..e1cb7f7
--- /dev/null
+++ b/packages/classy-store/tests/compiler/compileConsumer.mjs
@@ -0,0 +1,63 @@
+import {transformSync} from '@babel/core';
+import reactCompiler from 'babel-plugin-react-compiler';
+
+/** Compile consumer components without changing the compiler's inference rules. */
+export function compileConsumer(
+ source,
+ filename,
+ target = '18',
+ expectedFunctions = [],
+) {
+ const events = [];
+ const result = transformSync(source, {
+ filename,
+ configFile: false,
+ babelrc: false,
+ parserOpts: {sourceType: 'module', plugins: ['jsx', 'typescript']},
+ plugins: [
+ [
+ reactCompiler,
+ {
+ target,
+ logger: {
+ logEvent(_filename, event) {
+ events.push(event);
+ },
+ },
+ },
+ ],
+ ],
+ });
+ const failures = events.filter((event) =>
+ ['CompileError', 'CompileSkip', 'PipelineError'].includes(event.kind),
+ );
+ if (failures.length) {
+ throw new Error(
+ `React Compiler skipped ${filename}: ${JSON.stringify(failures)}`,
+ );
+ }
+ const compiledFunctions = events.filter(
+ (event) => event.kind === 'CompileSuccess' && event.memoSlots > 0,
+ );
+ const names = expectedFunctions.length
+ ? expectedFunctions
+ : [...source.matchAll(/\bfunction\s+([A-Z]\w*)\s*\(/g)].map(
+ (match) => match[1],
+ );
+ if (!names.length) {
+ throw new Error(`No named consumer components found in ${filename}`);
+ }
+ for (const name of names) {
+ if (!compiledFunctions.some((event) => event.fnName === name)) {
+ throw new Error(
+ `${filename}: ${name} did not compile with memo slots; ${JSON.stringify(events)}`,
+ );
+ }
+ }
+ const runtime =
+ target === '19' ? 'react/compiler-runtime' : 'react-compiler-runtime';
+ if (!result?.code?.includes(`from "${runtime}"`)) {
+ throw new Error(`${filename}: missing compiler memo-cache runtime import`);
+ }
+ return {code: result.code, compiledFunctions};
+}
diff --git a/packages/classy-store/tests/compiler/compiler.memory.test.tsx b/packages/classy-store/tests/compiler/compiler.memory.test.tsx
new file mode 100644
index 0000000..06ec5c2
--- /dev/null
+++ b/packages/classy-store/tests/compiler/compiler.memory.test.tsx
@@ -0,0 +1,297 @@
+import {afterEach, describe, expect, test} from 'bun:test';
+import {getVersion, snapshot, subscribe} from '@codebelt/classy-store';
+import {StrictMode} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {MemoryScope} from './fixtures/memoryScope/MemoryScope';
+import {MemorySelector} from './fixtures/memorySelector/MemorySelector';
+import {MemoryTracked} from './fixtures/memoryTracked/MemoryTracked';
+import {
+ createMemoryStore,
+ type MemoryStore,
+ memoryGetterReads,
+} from './memory.store';
+import {act} from './reactAct';
+
+const mounted = new Map();
+const cases = [
+ {description: 'selectors', Component: MemorySelector},
+ {description: 'auto-tracking', Component: MemoryTracked},
+];
+
+function mountRoot() {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mounted.set(root, container);
+ return {root, container};
+}
+
+function createCounters() {
+ const commits: string[] = [];
+ let activeEffects = 0;
+ return {
+ commits,
+ onCommit: (value: string) => {
+ commits.push(value);
+ },
+ onLifetime: (phase: 'setup' | 'cleanup') => {
+ activeEffects += phase === 'setup' ? 1 : -1;
+ },
+ getActiveEffects: () => activeEffects,
+ };
+}
+
+async function expectRetiredStoresInactive(stores: MemoryStore[]) {
+ const previousReads = stores.map((store) => memoryGetterReads.get(store.id));
+ await act(async () => {
+ for (const store of stores) {
+ store.rows[0].title += ' retired';
+ }
+ });
+ // A leaked external-store callback would evaluate the changed getter even if
+ // React ignored the update because its component had already unmounted.
+ expect(stores.map((store) => memoryGetterReads.get(store.id))).toEqual(
+ previousReads,
+ );
+}
+
+afterEach(async () => {
+ for (const [root, container] of mounted) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+ mounted.clear();
+ memoryGetterReads.clear();
+});
+
+describe('repeated React store lifecycles', () => {
+ test.each(
+ cases,
+ )('$description releases subscriptions after repeated StrictMode mounts', async ({
+ Component,
+ }) => {
+ const {root, container} = mountRoot();
+ const counters = createCounters();
+ const retiredStores: MemoryStore[] = [];
+ for (let cycle = 0; cycle < 20; cycle++) {
+ const store = createMemoryStore(`mount-${cycle}`);
+ await act(async () =>
+ root.render(
+
+
+ ,
+ ),
+ );
+ expect(counters.getActiveEffects()).toBe(1);
+ expect(memoryGetterReads.get(store.id)).toBeGreaterThan(0);
+ counters.commits.length = 0;
+ await act(async () => {
+ store.rows[0].title = `Current ${cycle}`;
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(container.textContent).toContain(`Current ${cycle}`);
+ await act(async () => root.render(null));
+ expect(counters.getActiveEffects()).toBe(0);
+ retiredStores.push(store);
+ }
+ counters.commits.length = 0;
+ await expectRetiredStoresInactive(retiredStores);
+ expect(counters.commits).toEqual([]);
+ expect(container.textContent).toBe('');
+ });
+
+ test.each(
+ cases,
+ )('$description releases prior local stores after keyed resets', async ({
+ Component,
+ }) => {
+ const {root, container} = mountRoot();
+ const counters = createCounters();
+ const observedStores = new Set();
+ const onStore = (store: MemoryStore) => {
+ observedStores.add(store);
+ };
+ for (let cycle = 0; cycle < 16; cycle++) {
+ await act(async () =>
+ root.render(
+
+
+ ,
+ ),
+ );
+ const stores = [...observedStores];
+ const currentStore = stores[stores.length - 1];
+ expect(currentStore.id).toBe(`reset-${cycle}`);
+ expect(stores).toHaveLength(cycle + 1);
+ expect(counters.getActiveEffects()).toBe(1);
+ counters.commits.length = 0;
+ await expectRetiredStoresInactive(stores.slice(0, -1));
+ expect(counters.commits).toEqual([]);
+ await act(async () => {
+ currentStore.rows[0].title = `Reset ${cycle}`;
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(container.textContent).toContain(`Reset ${cycle}`);
+ }
+ await act(async () => root.render(null));
+ expect(counters.getActiveEffects()).toBe(0);
+ counters.commits.length = 0;
+ await expectRetiredStoresInactive([...observedStores]);
+ expect(counters.commits).toEqual([]);
+ });
+
+ test.each(
+ cases,
+ )('$description switches stores without leaving callbacks on retired instances', async ({
+ Component,
+ }) => {
+ const {root, container} = mountRoot();
+ const counters = createCounters();
+ const stores: MemoryStore[] = [];
+ for (let cycle = 0; cycle < 16; cycle++) {
+ const store = createMemoryStore(`switch-${cycle}`);
+ stores.push(store);
+ await act(async () =>
+ root.render(
+ ,
+ ),
+ );
+ counters.commits.length = 0;
+ await expectRetiredStoresInactive(stores.slice(0, -1));
+ expect(counters.commits).toEqual([]);
+ await act(async () => {
+ container.querySelector('button')?.click();
+ });
+ expect(counters.commits).toHaveLength(1);
+ counters.commits.length = 0;
+ await act(async () => {
+ store.rows[0].title = `Current ${cycle}`;
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(container.textContent).toContain(
+ `Store ${cycle}:Stable:Current ${cycle}`,
+ );
+ }
+ await act(async () => root.render(null));
+ expect(counters.getActiveEffects()).toBe(0);
+ await expectRetiredStoresInactive(stores);
+ });
+
+ test.each(
+ cases,
+ )('$description isolates replacement collections and preserves stable child reads', async ({
+ Component,
+ }) => {
+ const {root, container} = mountRoot();
+ const store = createMemoryStore('collections');
+ const counters = createCounters();
+ await act(async () =>
+ root.render( ),
+ );
+ const stableSnapshot = snapshot(store).stable;
+ const retiredRows: MemoryStore['rows'] = [];
+ for (let cycle = 0; cycle < 24; cycle++) {
+ retiredRows.push(store.rows[0]);
+ counters.commits.length = 0;
+ await act(async () => {
+ store.rows = Array.from({length: 8}, (_, index) => ({
+ title: `Collection ${cycle} row ${index}`,
+ }));
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(snapshot(store).stable).toBe(stableSnapshot);
+ expect(container.textContent).toContain(`Collection ${cycle} row 7`);
+ await act(async () =>
+ root.render(
+ ,
+ ),
+ );
+ await act(async () => {
+ container.querySelector('button')?.click();
+ });
+ counters.commits.length = 0;
+ await act(async () => {
+ store.rows[0].title = `Current ${cycle}`;
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(container.textContent).toContain(
+ `Parent ${cycle}:Stable:Current ${cycle}`,
+ );
+ counters.commits.length = 0;
+ await act(async () => {
+ store.ignored++;
+ });
+ expect(counters.commits).toEqual([]);
+ }
+ const previousVersion = getVersion(store);
+ let notifications = 0;
+ const unsubscribe = subscribe(
+ store,
+ () => {
+ notifications++;
+ },
+ {sync: true},
+ );
+ try {
+ await act(async () => {
+ for (const row of retiredRows) row.title = 'Detached row';
+ });
+ expect(getVersion(store)).toBe(previousVersion);
+ expect(notifications).toBe(0);
+ expect(counters.commits).toEqual([]);
+ expect(container.textContent).toContain('Current 23');
+ } finally {
+ unsubscribe();
+ }
+ await act(async () => {
+ store.stable.label = 'Still subscribed';
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(container.textContent).toContain('Still subscribed:Current 23');
+ });
+
+ test.each(
+ cases,
+ )('$description detaches removed rows through repeated truncation and refill', async ({
+ Component,
+ }) => {
+ const {root, container} = mountRoot();
+ const store = createMemoryStore('truncate');
+ const counters = createCounters();
+ await act(async () =>
+ root.render( ),
+ );
+ for (let cycle = 0; cycle < 12; cycle++) {
+ const removedRow = store.rows[store.rows.length - 1];
+ counters.commits.length = 0;
+ await act(async () => {
+ store.rows.length = 0;
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(container.querySelector('output')?.textContent).toBe(
+ 'Rows:Stable:',
+ );
+ const versionAfterTruncation = getVersion(store);
+ counters.commits.length = 0;
+ await act(async () => {
+ removedRow.title = 'Retired';
+ });
+ expect(getVersion(store)).toBe(versionAfterTruncation);
+ expect(counters.commits).toEqual([]);
+ await act(async () => {
+ store.rows.push({title: `Refilled ${cycle}`}, {title: 'Last'});
+ });
+ expect(counters.commits).toHaveLength(1);
+ expect(container.querySelector('output')?.textContent).toBe(
+ `Rows:Stable:Refilled ${cycle},Last`,
+ );
+ }
+ });
+});
diff --git a/packages/classy-store/tests/compiler/compiler.qa.test.tsx b/packages/classy-store/tests/compiler/compiler.qa.test.tsx
new file mode 100644
index 0000000..56565f6
--- /dev/null
+++ b/packages/classy-store/tests/compiler/compiler.qa.test.tsx
@@ -0,0 +1,193 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {createClassyStore, type Snapshot} from '@codebelt/classy-store';
+import {prepareStoreForHydration} from '@codebelt/classy-store/react';
+import {type ReactNode, Suspense, startTransition} from 'react';
+import {createRoot, hydrateRoot, type Root} from 'react-dom/client';
+import {renderToString} from 'react-dom/server';
+import {QaHydrationTracked} from './fixtures/qaHydrationTracked/QaHydrationTracked';
+import {QaOpaqueDerivation} from './fixtures/qaOpaqueDerivation/QaOpaqueDerivation';
+import {QaReflectiveReads} from './fixtures/qaReflectiveReads/QaReflectiveReads';
+import {QaSelectorTransition} from './fixtures/qaSelectorTransition/QaSelectorTransition';
+import {QaTrackedTransition} from './fixtures/qaTrackedTransition/QaTrackedTransition';
+import type {QaGate, QaStore} from './qa.types';
+import {act} from './reactAct';
+
+const mounted = new Map();
+
+function createQaStore() {
+ return createClassyStore({
+ name: 'initial',
+ count: 0,
+ ignored: 0,
+ node: {first: 'First', second: 'Second'},
+ });
+}
+
+function createContainer(html = '') {
+ const container = document.createElement('div');
+ container.innerHTML = html;
+ document.body.appendChild(container);
+ return container;
+}
+
+function mount(element: ReactNode) {
+ const container = createContainer();
+ const root = createRoot(container);
+ mounted.set(root, container);
+ act(() => root.render(element));
+ return {root, container};
+}
+
+function createGate() {
+ let resolve!: () => void;
+ const gate: QaGate = {
+ promise: new Promise((resolvePromise) => {
+ resolve = resolvePromise;
+ }),
+ attempts: [],
+ released: false,
+ };
+ return {
+ gate,
+ release() {
+ gate.released = true;
+ resolve();
+ },
+ };
+}
+
+afterEach(async () => {
+ for (const [root, container] of mounted) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+ mounted.clear();
+});
+
+describe('independent compiled-consumer QA', () => {
+ test('retains reads inside a compiler-memoized helper after a parent-only rerender', async () => {
+ const store = createQaStore();
+ const {root, container} = mount(
+ ,
+ );
+ expect(container.textContent).toBe('original:INITIAL:0');
+
+ act(() => root.render( ));
+ expect(container.textContent).toBe('parent:INITIAL:0');
+
+ await act(async () => {
+ store.name = 'changed';
+ });
+ expect(container.textContent).toBe('parent:CHANGED:0');
+
+ await act(async () => {
+ store.count = 1;
+ });
+ expect(container.textContent).toBe('parent:CHANGED:1');
+ });
+
+ for (const kind of ['has', 'own', 'keys'] as const) {
+ test(`retains compiler-memoized ${kind} reads across unchanged snapshots`, async () => {
+ const store = createQaStore();
+ const {root, container} = mount(
+ ,
+ );
+ act(() =>
+ root.render(
+ ,
+ ),
+ );
+ const initial = kind === 'keys' ? 'first,second' : 'false';
+ expect(container.textContent).toBe(`parent:${initial}:0`);
+
+ await act(async () => {
+ store.node.optional = 'present';
+ });
+ const changed = kind === 'keys' ? 'first,second,optional' : 'true';
+ expect(container.textContent).toBe(`parent:${changed}:0`);
+
+ act(() =>
+ root.render(
+ ,
+ ),
+ );
+ await act(async () => {
+ delete store.node.optional;
+ });
+ expect(container.textContent).toBe(`again:${initial}:0`);
+ });
+ }
+
+ for (const [mode, Component] of [
+ ['tracked', QaTrackedTransition],
+ ['selector', QaSelectorTransition],
+ ] as const) {
+ test(`keeps ${mode} committed reads active through a suspended and abandoned transition`, async () => {
+ const store = createQaStore();
+ const {gate, release} = createGate();
+ const view = (field: 'first' | 'second') => (
+
+
+
+ );
+ const {root, container} = mount(view('first'));
+ await act(async () => startTransition(() => root.render(view('second'))));
+ expect(gate.attempts).toContain('second');
+ expect(container.textContent).toBe('First');
+
+ await act(async () => {
+ store.node.first = 'Committed';
+ });
+ expect(container.textContent).toBe('Committed');
+
+ act(() => root.render(view('first')));
+ gate.attempts.length = 0;
+ await act(async () => {
+ store.node.second = 'Abandoned';
+ });
+ expect(gate.attempts).toEqual([]);
+ expect(container.textContent).toBe('Committed');
+ await act(async () => release());
+
+ await act(async () => {
+ store.node.first = 'Still current';
+ });
+ expect(container.textContent).toBe('Still current');
+ });
+ }
+
+ test('preserves a compiled hydration snapshot identity when only unread data changes', async () => {
+ const store = prepareStoreForHydration(createQaStore());
+ const commits: Snapshot[] = [];
+ const onCommit = (state: Snapshot) => commits.push(state);
+ const container = createContainer(
+ renderToString( ),
+ );
+ const serverNode = container.firstElementChild;
+ store.ignored = 1;
+ const onRecoverableError = mock(() => {});
+
+ await act(async () => {
+ const root = hydrateRoot(
+ container,
+ ,
+ {onRecoverableError},
+ );
+ mounted.set(root, container);
+ });
+ expect(container.firstElementChild).toBe(serverNode);
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(commits).toHaveLength(1);
+
+ await act(async () => {
+ store.ignored = 2;
+ });
+ expect(commits).toHaveLength(1);
+ await act(async () => {
+ store.name = 'live';
+ });
+ expect(container.textContent).toBe('live');
+ expect(commits).toHaveLength(2);
+ expect(commits[0]).not.toBe(commits[1]);
+ });
+});
diff --git a/packages/classy-store/tests/compiler/compiler.scale.test.tsx b/packages/classy-store/tests/compiler/compiler.scale.test.tsx
new file mode 100644
index 0000000..49e3a92
--- /dev/null
+++ b/packages/classy-store/tests/compiler/compiler.scale.test.tsx
@@ -0,0 +1,250 @@
+import {afterEach, describe, expect, test} from 'bun:test';
+import type {ComponentType} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {ScaleSelectorField} from './fixtures/scaleSelectorField/ScaleSelectorField';
+import {ScaleSelectorRow} from './fixtures/scaleSelectorRow/ScaleSelectorRow';
+import {ScaleTrackedField} from './fixtures/scaleTrackedField/ScaleTrackedField';
+import {ScaleTrackedRow} from './fixtures/scaleTrackedRow/ScaleTrackedRow';
+import {act} from './reactAct';
+import {createScaleStore} from './scale.store';
+import type {ScaleConsumerProps} from './scale.types';
+
+const mounted = new Map();
+const formSize = 256;
+const listSize = 512;
+const fieldCases = [
+ {description: 'selectors', Component: ScaleSelectorField},
+ {description: 'auto-tracking', Component: ScaleTrackedField},
+];
+const rowCases = [
+ {description: 'selectors', Component: ScaleSelectorRow},
+ {description: 'auto-tracking', Component: ScaleTrackedRow},
+];
+
+afterEach(async () => {
+ for (const [root, container] of mounted) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+ mounted.clear();
+});
+
+async function mountConsumers(
+ Component: ComponentType,
+ size: number,
+ kind: 'form' | 'list',
+) {
+ const store = createScaleStore(size);
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mounted.set(root, container);
+ const commits: number[] = [];
+ const onCommit = (index: number) => commits.push(index);
+ const consumers = Array.from({length: size}, (_, index) => ({
+ id: `consumer-${index}`,
+ index,
+ }));
+ const render = (label = 'Initial', count = size) => {
+ const children = consumers
+ .slice(0, count)
+ .map(({id, index}) => (
+
+ ));
+ root.render(
+ kind === 'list' ? : ,
+ );
+ };
+ await act(async () => render());
+ expect(commits).toHaveLength(size);
+ commits.length = 0;
+ const readConsumer = (index: number) =>
+ container.querySelector(`[data-consumer="${index}"]`);
+ const readInput = (index: number) =>
+ readConsumer(index)?.querySelector('input')?.value;
+ const readOutput = (index: number) =>
+ readConsumer(index)?.querySelector('output')?.textContent;
+ const clickLocal = (index: number) =>
+ readConsumer(index)?.querySelector('button')?.click();
+ return {
+ store,
+ commits,
+ render,
+ readInput,
+ readOutput,
+ readConsumer,
+ clickLocal,
+ root,
+ container,
+ };
+}
+
+describe('large controlled forms', () => {
+ test.each(
+ fieldCases,
+ )('$description commits only fields with changed values', async ({
+ Component,
+ }) => {
+ const {store, commits, readInput} = await mountConsumers(
+ Component,
+ formSize,
+ 'form',
+ );
+ await act(async () => {
+ store.ignored++;
+ store.rows[0].title = 'Unread row';
+ store.fields[137].value = 'Field 137';
+ });
+ expect(commits).toEqual([]);
+
+ await act(async () => {
+ store.fields[137].value = 'Typed value';
+ });
+ expect(commits).toEqual([137]);
+ expect(readInput(137)).toBe('Typed value');
+ expect(readInput(136)).toBe('Field 136');
+
+ commits.length = 0;
+ await act(async () => {
+ store.fields[137].value = 'Typed value';
+ });
+ expect(commits).toEqual([]);
+
+ await act(async () => {
+ for (const index of [1, 64, 128, 255]) {
+ store.fields[index].value = `Updated ${index}`;
+ }
+ });
+ expect([...commits].sort((left, right) => left - right)).toEqual([
+ 1, 64, 128, 255,
+ ]);
+ for (const index of [1, 64, 128, 255]) {
+ expect(readInput(index)).toBe(`Updated ${index}`);
+ }
+ });
+
+ test.each(
+ fieldCases,
+ )('$description preserves one-field updates after parent and local renders', async ({
+ Component,
+ }) => {
+ const {store, commits, render, clickLocal, readInput, readConsumer} =
+ await mountConsumers(Component, formSize, 'form');
+ await act(async () => render('Parent'));
+ expect(commits).toHaveLength(formSize);
+ expect(readConsumer(137)?.textContent).toContain('Parent 137');
+ commits.length = 0;
+
+ await act(async () => clickLocal(137));
+ expect(commits).toEqual([137]);
+ expect(readConsumer(137)?.textContent).toContain('Local 1');
+ commits.length = 0;
+
+ await act(async () => {
+ store.fields[137].value = 'After memoized render';
+ });
+ expect(commits).toEqual([137]);
+ expect(readInput(137)).toBe('After memoized render');
+ commits.length = 0;
+ await act(async () => {
+ store.ignored++;
+ });
+ expect(commits).toEqual([]);
+ });
+});
+
+describe('large lists', () => {
+ test.each(
+ rowCases,
+ )('$description keeps row updates isolated through removal and remount', async ({
+ Component,
+ }) => {
+ const {store, commits, render, readOutput, root, container} =
+ await mountConsumers(Component, listSize, 'list');
+ await act(async () => {
+ store.ignored++;
+ store.fields[0].value = 'Unread field';
+ store.rows[400].title = 'Row 400';
+ });
+ expect(commits).toEqual([]);
+
+ await act(async () => {
+ store.rows[400].title = 'Updated';
+ store.rows[400].count = 900;
+ });
+ expect(commits).toEqual([400]);
+ expect(readOutput(400)).toBe('Initial:Updated:900');
+ expect(readOutput(399)).toBe('Initial:Row 399:399');
+
+ await act(async () => render('Initial', listSize / 2));
+ commits.length = 0;
+ await act(async () => {
+ store.rows[400].title = 'While unmounted';
+ });
+ expect(commits).toEqual([]);
+ expect(readOutput(400)).toBeUndefined();
+ await act(async () => {
+ store.rows[100].count++;
+ });
+ expect(commits).toEqual([100]);
+ expect(readOutput(100)).toBe('Initial:Row 100:101');
+
+ await act(async () => render());
+ expect(readOutput(400)).toBe('Initial:While unmounted:900');
+ commits.length = 0;
+ await act(async () => {
+ store.rows[400].count++;
+ });
+ expect(commits).toEqual([400]);
+ expect(readOutput(400)).toBe('Initial:While unmounted:901');
+
+ await act(async () => root.unmount());
+ mounted.delete(root);
+ container.remove();
+ commits.length = 0;
+ await act(async () => {
+ store.rows[100].title = 'No mounted consumers';
+ store.rows[400].count++;
+ });
+ expect(commits).toEqual([]);
+ });
+
+ test.each(
+ rowCases,
+ )('$description retains memoized reads without waking sibling rows', async ({
+ Component,
+ }) => {
+ const {store, commits, render, clickLocal, readOutput} =
+ await mountConsumers(Component, listSize, 'list');
+ await act(async () => render('Parent'));
+ expect(commits).toHaveLength(listSize);
+ commits.length = 0;
+
+ await act(async () => clickLocal(400));
+ expect(commits).toEqual([400]);
+ commits.length = 0;
+ await act(async () => {
+ store.rows[400].title = 'After local render';
+ });
+ expect(commits).toEqual([400]);
+ expect(readOutput(400)).toBe('Parent:After local render:400');
+
+ commits.length = 0;
+ await act(async () => {
+ store.rows[400].count = 401;
+ });
+ expect(commits).toEqual([400]);
+ expect(readOutput(400)).toBe('Parent:After local render:401');
+ commits.length = 0;
+ await act(async () => {
+ store.ignored++;
+ });
+ expect(commits).toEqual([]);
+ });
+});
diff --git a/packages/classy-store/tests/compiler/compiler.store.ts b/packages/classy-store/tests/compiler/compiler.store.ts
new file mode 100644
index 0000000..fe5f7f2
--- /dev/null
+++ b/packages/classy-store/tests/compiler/compiler.store.ts
@@ -0,0 +1,19 @@
+import {createClassyStore} from '@codebelt/classy-store';
+import {createStoreHook} from '@codebelt/classy-store/react';
+
+class CompilerStore {
+ count = 0;
+ unrelated = 0;
+ profile = {name: 'Ada', ignored: 0};
+ groups = [{name: 'Alpha'}, {name: 'Beta'}];
+}
+
+export const compilerStore = createClassyStore(new CompilerStore());
+export const useCompilerStore = createStoreHook(compilerStore);
+
+export function resetCompilerStore() {
+ compilerStore.count = 0;
+ compilerStore.unrelated = 0;
+ compilerStore.profile = {name: 'Ada', ignored: 0};
+ compilerStore.groups = [{name: 'Alpha'}, {name: 'Beta'}];
+}
diff --git a/packages/classy-store/tests/compiler/compiler.types.ts b/packages/classy-store/tests/compiler/compiler.types.ts
new file mode 100644
index 0000000..593adc4
--- /dev/null
+++ b/packages/classy-store/tests/compiler/compiler.types.ts
@@ -0,0 +1,7 @@
+export type Selection = {name: string};
+
+export type ConsumerProps = {
+ onCommit: (value: string) => void;
+ isEqual?: (previous: Selection, next: Selection) => boolean;
+ prefix?: string;
+};
diff --git a/packages/classy-store/tests/compiler/components/boundSelector/BoundSelector.tsx b/packages/classy-store/tests/compiler/components/boundSelector/BoundSelector.tsx
new file mode 100644
index 0000000..d010d6b
--- /dev/null
+++ b/packages/classy-store/tests/compiler/components/boundSelector/BoundSelector.tsx
@@ -0,0 +1,30 @@
+import {useLayoutEffect, useState} from 'react';
+import {useCompilerStore} from '../../compiler.store';
+import type {ConsumerProps} from '../../compiler.types';
+import {MemoProfileName} from '../profileName/ProfileName';
+
+export function BoundSelector({onCommit, isEqual, prefix = ''}: ConsumerProps) {
+ const [index, setIndex] = useState(0);
+ const count = useCompilerStore((state) => state.count);
+ const profile = useCompilerStore((state) => state.profile);
+ const selected = useCompilerStore(
+ (state) => ({name: `${prefix}${state.groups[index].name}`}),
+ {isEqual: isEqual ?? ((previous, next) => previous.name === next.name)},
+ );
+ const value = `${count}|${selected.name}`;
+ useLayoutEffect(() => {
+ onCommit(value);
+ });
+ return (
+
+ setIndex((previous) => 1 - previous)}
+ >
+ Switch group
+
+ {value}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/components/boundTracked/BoundTracked.tsx b/packages/classy-store/tests/compiler/components/boundTracked/BoundTracked.tsx
new file mode 100644
index 0000000..eea1b69
--- /dev/null
+++ b/packages/classy-store/tests/compiler/components/boundTracked/BoundTracked.tsx
@@ -0,0 +1,27 @@
+import {useLayoutEffect, useState} from 'react';
+import {useCompilerStore} from '../../compiler.store';
+import type {ConsumerProps} from '../../compiler.types';
+import {MemoProfileName} from '../profileName/ProfileName';
+
+export function BoundTracked({onCommit}: ConsumerProps) {
+ const state = useCompilerStore();
+ const [index, setIndex] = useState(0);
+ const names = state.groups.map((group) => group.name).join(',');
+ const value = `${state.count}|${state.groups[index].name}`;
+ useLayoutEffect(() => {
+ onCommit(value);
+ });
+ return (
+
+ setIndex((previous) => 1 - previous)}
+ >
+ Switch group
+
+ {value}
+ {names}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/components/directSelector/DirectSelector.tsx b/packages/classy-store/tests/compiler/components/directSelector/DirectSelector.tsx
new file mode 100644
index 0000000..fa4c672
--- /dev/null
+++ b/packages/classy-store/tests/compiler/components/directSelector/DirectSelector.tsx
@@ -0,0 +1,36 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import {compilerStore} from '../../compiler.store';
+import type {ConsumerProps} from '../../compiler.types';
+import {MemoProfileName} from '../profileName/ProfileName';
+
+export function DirectSelector({
+ onCommit,
+ isEqual,
+ prefix = '',
+}: ConsumerProps) {
+ const [index, setIndex] = useState(0);
+ const count = useClassyStore(compilerStore, (state) => state.count);
+ const profile = useClassyStore(compilerStore, (state) => state.profile);
+ const selected = useClassyStore(
+ compilerStore,
+ (state) => ({name: `${prefix}${state.groups[index].name}`}),
+ {isEqual: isEqual ?? ((previous, next) => previous.name === next.name)},
+ );
+ const value = `${count}|${selected.name}`;
+ useLayoutEffect(() => {
+ onCommit(value);
+ });
+ return (
+
+ setIndex((previous) => 1 - previous)}
+ >
+ Switch group
+
+ {value}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/components/directTracked/DirectTracked.tsx b/packages/classy-store/tests/compiler/components/directTracked/DirectTracked.tsx
new file mode 100644
index 0000000..14413dc
--- /dev/null
+++ b/packages/classy-store/tests/compiler/components/directTracked/DirectTracked.tsx
@@ -0,0 +1,28 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import {compilerStore} from '../../compiler.store';
+import type {ConsumerProps} from '../../compiler.types';
+import {MemoProfileName} from '../profileName/ProfileName';
+
+export function DirectTracked({onCommit}: ConsumerProps) {
+ const state = useClassyStore(compilerStore);
+ const [index, setIndex] = useState(0);
+ const names = state.groups.map((group) => group.name).join(',');
+ const value = `${state.count}|${state.groups[index].name}`;
+ useLayoutEffect(() => {
+ onCommit(value);
+ });
+ return (
+
+ setIndex((previous) => 1 - previous)}
+ >
+ Switch group
+
+ {value}
+ {names}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/components/profileName/ProfileName.tsx b/packages/classy-store/tests/compiler/components/profileName/ProfileName.tsx
new file mode 100644
index 0000000..657e02a
--- /dev/null
+++ b/packages/classy-store/tests/compiler/components/profileName/ProfileName.tsx
@@ -0,0 +1,9 @@
+import {memo} from 'react';
+
+type Props = {profile: {readonly name: string}};
+
+function ProfileName({profile}: Props) {
+ return {profile.name} ;
+}
+
+export const MemoProfileName = memo(ProfileName);
diff --git a/packages/classy-store/tests/compiler/consumer.fixture.tsx b/packages/classy-store/tests/compiler/consumer.fixture.tsx
new file mode 100644
index 0000000..315ccf5
--- /dev/null
+++ b/packages/classy-store/tests/compiler/consumer.fixture.tsx
@@ -0,0 +1,149 @@
+import {afterEach, beforeEach, describe, expect, test} from 'bun:test';
+import {type ComponentType, StrictMode} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {compilerStore, resetCompilerStore} from './compiler.store';
+import type {ConsumerProps} from './compiler.types';
+import {BoundSelector} from './components/boundSelector/BoundSelector';
+import {BoundTracked} from './components/boundTracked/BoundTracked';
+import {DirectSelector} from './components/directSelector/DirectSelector';
+import {DirectTracked} from './components/directTracked/DirectTracked';
+import {act} from './reactAct';
+
+const mounted: {root: Root; container: HTMLDivElement}[] = [];
+
+beforeEach(resetCompilerStore);
+
+afterEach(async () => {
+ for (const {root, container} of mounted.splice(0)) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+});
+
+async function mount(Component: ComponentType) {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mounted.push({root, container});
+ const commits: string[] = [];
+ const onCommit = (value: string) => commits.push(value);
+ const render = (props: Partial = {}) => {
+ root.render(
+
+
+ ,
+ );
+ };
+ await act(async () => render());
+ commits.length = 0;
+ const read = () => container.querySelector('output')?.textContent;
+ const profile = () => container.querySelector('[data-profile]')?.textContent;
+ const switchGroup = () => container.querySelector('button')?.click();
+ return {container, root, commits, render, read, profile, switchGroup};
+}
+
+for (const Component of [
+ DirectTracked,
+ BoundTracked,
+ DirectSelector,
+ BoundSelector,
+]) {
+ describe(`compiled ${Component.name}`, () => {
+ test('keeps memoized reads subscribed after a local render', async () => {
+ const {read, profile, switchGroup} = await mount(Component);
+ expect(read()).toBe('0|Alpha');
+ expect(profile()).toBe('Ada');
+ act(switchGroup);
+ expect(read()).toBe('0|Beta');
+
+ act(() => {
+ compilerStore.count = 1;
+ });
+ expect(read()).toBe('1|Beta');
+ act(() => {
+ compilerStore.profile.name = 'Grace';
+ });
+ expect(profile()).toBe('Grace');
+ act(() => {
+ compilerStore.groups[1].name = 'Updated';
+ });
+ expect(read()).toBe('1|Updated');
+ });
+
+ test('updates conditional nested reads and ignores unrelated fields', async () => {
+ const {read, commits, switchGroup} = await mount(Component);
+ act(() => {
+ compilerStore.unrelated++;
+ });
+ expect(commits).toEqual([]);
+ act(() => {
+ compilerStore.groups[0].name = 'Active';
+ });
+ expect(read()).toBe('0|Active');
+ act(switchGroup);
+ expect(read()).toBe('0|Beta');
+ act(() => {
+ compilerStore.groups[1].name = 'Second';
+ });
+ expect(read()).toBe('0|Second');
+ act(switchGroup);
+ expect(read()).toBe('0|Active');
+ act(() => {
+ compilerStore.groups[0].name = 'Again';
+ });
+ expect(read()).toBe('0|Again');
+ });
+
+ test('keeps nested props live when a memoized child skips a render', async () => {
+ const {profile, switchGroup} = await mount(Component);
+ act(switchGroup);
+ act(() => {
+ compilerStore.groups[1].name = 'Local group';
+ });
+ expect(profile()).toBe('Ada');
+ act(() => {
+ compilerStore.profile = {name: 'Katherine', ignored: 0};
+ });
+ expect(profile()).toBe('Katherine');
+ act(switchGroup);
+ act(() => {
+ compilerStore.profile.name = 'Dorothy';
+ });
+ expect(profile()).toBe('Dorothy');
+ });
+ });
+}
+
+for (const Component of [DirectSelector, BoundSelector]) {
+ test(`compiled ${Component.name} refreshes selectors and equality from props`, async () => {
+ const {read, render, switchGroup} = await mount(Component);
+ act(() => render({prefix: 'Selected: '}));
+ expect(read()).toBe('0|Selected: Alpha');
+ act(switchGroup);
+ expect(read()).toBe('0|Selected: Beta');
+ const ignoreChanges = () => true;
+ act(() => render({prefix: 'Selected: ', isEqual: ignoreChanges}));
+ act(() => {
+ compilerStore.groups[1].name = 'Newest';
+ });
+ expect(read()).toBe('0|Selected: Beta');
+ act(() => render({prefix: 'Selected: '}));
+ expect(read()).toBe('0|Selected: Newest');
+ });
+}
+
+for (const Component of [DirectTracked, BoundTracked]) {
+ test(`compiled ${Component.name} retains array reads used by a cached derivation`, async () => {
+ const {container, read, switchGroup} = await mount(Component);
+ const names = () => container.querySelector('[data-names]')?.textContent;
+ expect(names()).toBe('Alpha,Beta');
+ act(switchGroup);
+ expect(read()).toBe('0|Beta');
+ expect(names()).toBe('Alpha,Beta');
+ act(() => {
+ compilerStore.groups[0].name = 'Changed';
+ });
+ expect(read()).toBe('0|Beta');
+ expect(names()).toBe('Changed,Beta');
+ });
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/memoryScope/MemoryScope.tsx b/packages/classy-store/tests/compiler/fixtures/memoryScope/MemoryScope.tsx
new file mode 100644
index 0000000..7225e90
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/memoryScope/MemoryScope.tsx
@@ -0,0 +1,18 @@
+import {useLocalStore} from '@codebelt/classy-store/react';
+import {type ComponentType, useLayoutEffect} from 'react';
+import {MemoryStore} from '../../memory.store';
+import type {MemoryConsumerProps} from '../../memory.types';
+
+type Props = Omit & {
+ id: string;
+ Consumer: ComponentType;
+ onStore: (store: MemoryStore) => void;
+};
+
+export function MemoryScope({id, Consumer, onStore, ...props}: Props) {
+ const store = useLocalStore(() => new MemoryStore(id));
+ useLayoutEffect(() => {
+ onStore(store);
+ }, [onStore, store]);
+ return ;
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/memorySelector/MemorySelector.tsx b/packages/classy-store/tests/compiler/fixtures/memorySelector/MemorySelector.tsx
new file mode 100644
index 0000000..e69ca6e
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/memorySelector/MemorySelector.tsx
@@ -0,0 +1,36 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import type {MemoryConsumerProps} from '../../memory.types';
+
+export function MemorySelector({
+ store,
+ label,
+ onCommit,
+ onLifetime,
+}: MemoryConsumerProps) {
+ const value = useClassyStore(
+ store,
+ (state) => `${state.stable.label}:${state.summary}`,
+ );
+ const [localCount, setLocalCount] = useState(0);
+ useLayoutEffect(() => {
+ onCommit(value);
+ });
+ useLayoutEffect(() => {
+ onLifetime('setup');
+ return () => onLifetime('cleanup');
+ }, [onLifetime]);
+ return (
+
+
+ {label}:{value}
+
+ setLocalCount((previousCount) => previousCount + 1)}
+ >
+ Local {localCount}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/memoryTracked/MemoryTracked.tsx b/packages/classy-store/tests/compiler/fixtures/memoryTracked/MemoryTracked.tsx
new file mode 100644
index 0000000..7a0100a
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/memoryTracked/MemoryTracked.tsx
@@ -0,0 +1,34 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import type {MemoryConsumerProps} from '../../memory.types';
+
+export function MemoryTracked({
+ store,
+ label,
+ onCommit,
+ onLifetime,
+}: MemoryConsumerProps) {
+ const state = useClassyStore(store);
+ const value = `${state.stable.label}:${state.summary}`;
+ const [localCount, setLocalCount] = useState(0);
+ useLayoutEffect(() => {
+ onCommit(value);
+ });
+ useLayoutEffect(() => {
+ onLifetime('setup');
+ return () => onLifetime('cleanup');
+ }, [onLifetime]);
+ return (
+
+
+ {label}:{value}
+
+ setLocalCount((previousCount) => previousCount + 1)}
+ >
+ Local {localCount}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/qaHydrationTracked/QaHydrationTracked.tsx b/packages/classy-store/tests/compiler/fixtures/qaHydrationTracked/QaHydrationTracked.tsx
new file mode 100644
index 0000000..e64cb25
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/qaHydrationTracked/QaHydrationTracked.tsx
@@ -0,0 +1,11 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useEffect} from 'react';
+import type {QaHydrationProps} from '../../qa.types';
+
+export function QaHydrationTracked({store, onCommit}: QaHydrationProps) {
+ const state = useClassyStore(store);
+ useEffect(() => {
+ onCommit(state);
+ }, [state, onCommit]);
+ return {state.name} ;
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/qaOpaqueDerivation/QaOpaqueDerivation.tsx b/packages/classy-store/tests/compiler/fixtures/qaOpaqueDerivation/QaOpaqueDerivation.tsx
new file mode 100644
index 0000000..7321d66
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/qaOpaqueDerivation/QaOpaqueDerivation.tsx
@@ -0,0 +1,15 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {QaStore} from '../../qa.types';
+import {formatQaName} from '../../qa.utils';
+
+type Props = {store: QaStore; label: string};
+
+export function QaOpaqueDerivation({store, label}: Props) {
+ const state = useClassyStore(store);
+ const formatted = formatQaName(state);
+ return (
+
+ {label}:{formatted}:{state.count}
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/qaReflectiveReads/QaReflectiveReads.tsx b/packages/classy-store/tests/compiler/fixtures/qaReflectiveReads/QaReflectiveReads.tsx
new file mode 100644
index 0000000..eb82564
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/qaReflectiveReads/QaReflectiveReads.tsx
@@ -0,0 +1,15 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {QaReadKind, QaStore} from '../../qa.types';
+import {readQaShape} from '../../qa.utils';
+
+type Props = {store: QaStore; label: string; kind: QaReadKind};
+
+export function QaReflectiveReads({store, label, kind}: Props) {
+ const state = useClassyStore(store);
+ const shape = readQaShape(state, kind);
+ return (
+
+ {label}:{shape}:{state.count}
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/qaSelectorTransition/QaSelectorTransition.tsx b/packages/classy-store/tests/compiler/fixtures/qaSelectorTransition/QaSelectorTransition.tsx
new file mode 100644
index 0000000..c68bd9b
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/qaSelectorTransition/QaSelectorTransition.tsx
@@ -0,0 +1,9 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {QaTransitionProps} from '../../qa.types';
+import {readQaGate} from '../../qa.utils';
+
+export function QaSelectorTransition({store, field, gate}: QaTransitionProps) {
+ const value = useClassyStore(store, (state) => state.node[field]);
+ readQaGate(gate, field);
+ return {value} ;
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/qaTrackedTransition/QaTrackedTransition.tsx b/packages/classy-store/tests/compiler/fixtures/qaTrackedTransition/QaTrackedTransition.tsx
new file mode 100644
index 0000000..dacf446
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/qaTrackedTransition/QaTrackedTransition.tsx
@@ -0,0 +1,10 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {QaTransitionProps} from '../../qa.types';
+import {readQaGate} from '../../qa.utils';
+
+export function QaTrackedTransition({store, field, gate}: QaTransitionProps) {
+ const state = useClassyStore(store);
+ const value = state.node[field];
+ readQaGate(gate, field);
+ return {value} ;
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/scaleSelectorField/ScaleSelectorField.tsx b/packages/classy-store/tests/compiler/fixtures/scaleSelectorField/ScaleSelectorField.tsx
new file mode 100644
index 0000000..b62671a
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/scaleSelectorField/ScaleSelectorField.tsx
@@ -0,0 +1,35 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import type {ScaleConsumerProps} from '../../scale.types';
+
+export function ScaleSelectorField({
+ store,
+ index,
+ label,
+ onCommit,
+}: ScaleConsumerProps) {
+ const value = useClassyStore(store, (state) => state.fields[index].value);
+ const [localCount, setLocalCount] = useState(0);
+ useLayoutEffect(() => {
+ onCommit(index);
+ });
+ return (
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/scaleSelectorRow/ScaleSelectorRow.tsx b/packages/classy-store/tests/compiler/fixtures/scaleSelectorRow/ScaleSelectorRow.tsx
new file mode 100644
index 0000000..fc61eae
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/scaleSelectorRow/ScaleSelectorRow.tsx
@@ -0,0 +1,32 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import type {ScaleConsumerProps} from '../../scale.types';
+
+export function ScaleSelectorRow({
+ store,
+ index,
+ label,
+ onCommit,
+}: ScaleConsumerProps) {
+ const value = useClassyStore(
+ store,
+ (state) => `${state.rows[index].title}:${state.rows[index].count}`,
+ );
+ const [localCount, setLocalCount] = useState(0);
+ useLayoutEffect(() => {
+ onCommit(index);
+ });
+ return (
+
+
+ {label}:{value}
+
+ setLocalCount((previousCount) => previousCount + 1)}
+ >
+ Local {localCount}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/scaleTrackedField/ScaleTrackedField.tsx b/packages/classy-store/tests/compiler/fixtures/scaleTrackedField/ScaleTrackedField.tsx
new file mode 100644
index 0000000..e0a0544
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/scaleTrackedField/ScaleTrackedField.tsx
@@ -0,0 +1,36 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import type {ScaleConsumerProps} from '../../scale.types';
+
+export function ScaleTrackedField({
+ store,
+ index,
+ label,
+ onCommit,
+}: ScaleConsumerProps) {
+ const state = useClassyStore(store);
+ const value = state.fields[index].value;
+ const [localCount, setLocalCount] = useState(0);
+ useLayoutEffect(() => {
+ onCommit(index);
+ });
+ return (
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/fixtures/scaleTrackedRow/ScaleTrackedRow.tsx b/packages/classy-store/tests/compiler/fixtures/scaleTrackedRow/ScaleTrackedRow.tsx
new file mode 100644
index 0000000..7effe7c
--- /dev/null
+++ b/packages/classy-store/tests/compiler/fixtures/scaleTrackedRow/ScaleTrackedRow.tsx
@@ -0,0 +1,30 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect, useState} from 'react';
+import type {ScaleConsumerProps} from '../../scale.types';
+
+export function ScaleTrackedRow({
+ store,
+ index,
+ label,
+ onCommit,
+}: ScaleConsumerProps) {
+ const state = useClassyStore(store);
+ const value = `${state.rows[index].title}:${state.rows[index].count}`;
+ const [localCount, setLocalCount] = useState(0);
+ useLayoutEffect(() => {
+ onCommit(index);
+ });
+ return (
+
+
+ {label}:{value}
+
+ setLocalCount((previousCount) => previousCount + 1)}
+ >
+ Local {localCount}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/compiler/memory.store.ts b/packages/classy-store/tests/compiler/memory.store.ts
new file mode 100644
index 0000000..00f6a3f
--- /dev/null
+++ b/packages/classy-store/tests/compiler/memory.store.ts
@@ -0,0 +1,21 @@
+import {createClassyStore} from '@codebelt/classy-store';
+
+// Keep counters by primitive id so the instrumentation cannot retain stores.
+export const memoryGetterReads = new Map();
+
+export class MemoryStore {
+ ignored = 0;
+ stable = {label: 'Stable'};
+ rows = Array.from({length: 8}, (_, index) => ({title: `Row ${index}`}));
+
+ constructor(public id: string) {}
+
+ get summary() {
+ memoryGetterReads.set(this.id, (memoryGetterReads.get(this.id) ?? 0) + 1);
+ return this.rows.map((row) => row.title).join(',');
+ }
+}
+
+export function createMemoryStore(id: string) {
+ return createClassyStore(new MemoryStore(id));
+}
diff --git a/packages/classy-store/tests/compiler/memory.types.ts b/packages/classy-store/tests/compiler/memory.types.ts
new file mode 100644
index 0000000..731c4ab
--- /dev/null
+++ b/packages/classy-store/tests/compiler/memory.types.ts
@@ -0,0 +1,8 @@
+import type {MemoryStore} from './memory.store';
+
+export type MemoryConsumerProps = {
+ store: MemoryStore;
+ label: string;
+ onCommit: (value: string) => void;
+ onLifetime: (phase: 'setup' | 'cleanup') => void;
+};
diff --git a/packages/classy-store/tests/compiler/qa.types.ts b/packages/classy-store/tests/compiler/qa.types.ts
new file mode 100644
index 0000000..a30a763
--- /dev/null
+++ b/packages/classy-store/tests/compiler/qa.types.ts
@@ -0,0 +1,27 @@
+import type {Snapshot} from '@codebelt/classy-store';
+
+export type QaStore = {
+ name: string;
+ count: number;
+ ignored: number;
+ node: {first: string; second: string; optional?: string};
+};
+
+export type QaReadKind = 'has' | 'own' | 'keys';
+
+export type QaGate = {
+ promise: Promise;
+ attempts: string[];
+ released: boolean;
+};
+
+export type QaTransitionProps = {
+ store: QaStore;
+ field: 'first' | 'second';
+ gate: QaGate;
+};
+
+export type QaHydrationProps = {
+ store: QaStore;
+ onCommit: (state: Snapshot) => void;
+};
diff --git a/packages/classy-store/tests/compiler/qa.utils.ts b/packages/classy-store/tests/compiler/qa.utils.ts
new file mode 100644
index 0000000..265d3ba
--- /dev/null
+++ b/packages/classy-store/tests/compiler/qa.utils.ts
@@ -0,0 +1,17 @@
+import type {Snapshot} from '@codebelt/classy-store';
+import type {QaGate, QaReadKind, QaStore} from './qa.types';
+
+export function formatQaName(state: Snapshot) {
+ return state.name.toUpperCase();
+}
+
+export function readQaGate(gate: QaGate, field: string) {
+ gate.attempts.push(field);
+ if (field === 'second' && !gate.released) throw gate.promise;
+}
+
+export function readQaShape(state: Snapshot, kind: QaReadKind) {
+ if (kind === 'has') return String('optional' in state.node);
+ if (kind === 'own') return String(Object.hasOwn(state.node, 'optional'));
+ return Reflect.ownKeys(state.node).join(',');
+}
diff --git a/packages/classy-store/tests/compiler/reactAct.ts b/packages/classy-store/tests/compiler/reactAct.ts
new file mode 100644
index 0000000..156b2d5
--- /dev/null
+++ b/packages/classy-store/tests/compiler/reactAct.ts
@@ -0,0 +1,4 @@
+import * as React from 'react';
+import {act as legacyAct} from 'react-dom/test-utils';
+
+export const act = React.act ?? legacyAct;
diff --git a/packages/classy-store/tests/compiler/runCompiler.mjs b/packages/classy-store/tests/compiler/runCompiler.mjs
new file mode 100644
index 0000000..9770f38
--- /dev/null
+++ b/packages/classy-store/tests/compiler/runCompiler.mjs
@@ -0,0 +1,179 @@
+import {spawnSync} from 'node:child_process';
+import {
+ cpSync,
+ existsSync,
+ mkdtempSync,
+ readdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs';
+import {tmpdir} from 'node:os';
+import {dirname, join, relative, resolve} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import {compileConsumer} from './compileConsumer.mjs';
+
+const fixturesDirectory = dirname(fileURLToPath(import.meta.url));
+const packageDirectory = resolve(fixturesDirectory, '../..');
+const packageManifest = JSON.parse(
+ readFileSync(join(packageDirectory, 'package.json'), 'utf8'),
+);
+const requestedVersions = process.argv.slice(2);
+const reactVersions = requestedVersions.length
+ ? requestedVersions
+ : ['18.0.0', '18.3.1', packageManifest.devDependencies.react];
+const bunExecutable = process.env.BUN_EXECUTABLE ?? 'bun';
+const temporaryDirectory = mkdtempSync(
+ join(tmpdir(), 'classy-store-compiler-'),
+);
+
+function run(command, argumentsList, cwd, captureOutput = false) {
+ const result = spawnSync(command, argumentsList, {
+ cwd,
+ encoding: 'utf8',
+ stdio: captureOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
+ env: {
+ ...process.env,
+ NODE_ENV: 'development',
+ npm_config_cache: join(temporaryDirectory, 'npm-cache'),
+ },
+ });
+ if (result.error || result.status !== 0) {
+ throw new Error(
+ `${command} ${argumentsList.join(' ')} failed\n${result.stderr ?? ''}`,
+ {cause: result.error},
+ );
+ }
+ return result.stdout;
+}
+
+try {
+ const packed = JSON.parse(
+ run(
+ 'npm',
+ [
+ 'pack',
+ '--ignore-scripts',
+ '--json',
+ '--pack-destination',
+ temporaryDirectory,
+ ],
+ packageDirectory,
+ true,
+ ),
+ );
+ const packageArchive = join(temporaryDirectory, packed[0].filename);
+ for (const reactVersion of reactVersions) {
+ const consumerDirectory = join(temporaryDirectory, `react-${reactVersion}`);
+ cpSync(fixturesDirectory, consumerDirectory, {recursive: true});
+ const target = reactVersion.startsWith('18.') ? '18' : '19';
+ const compilation = [];
+ const compiledSources = [];
+ for (const directory of ['components', 'fixtures']) {
+ const componentDirectory = join(fixturesDirectory, directory);
+ if (!existsSync(componentDirectory)) continue;
+ for (const path of readdirSync(componentDirectory, {recursive: true})) {
+ if (!path.endsWith('.tsx')) continue;
+ const sourcePath = join(componentDirectory, path);
+ const {code, compiledFunctions} = compileConsumer(
+ readFileSync(sourcePath, 'utf8'),
+ sourcePath,
+ target,
+ );
+ compiledSources.push({
+ path: join(
+ consumerDirectory,
+ relative(fixturesDirectory, sourcePath),
+ ),
+ code,
+ });
+ compilation.push(...compiledFunctions);
+ }
+ }
+ writeFileSync(
+ join(consumerDirectory, 'compilation.json'),
+ JSON.stringify({target, compiledFunctions: compilation}, null, 2),
+ );
+ writeFileSync(
+ join(consumerDirectory, 'package.json'),
+ JSON.stringify({
+ private: true,
+ type: 'module',
+ dependencies: {
+ '@codebelt/classy-store': `file:${packageArchive}`,
+ react: reactVersion,
+ 'react-dom': reactVersion,
+ 'react-compiler-runtime':
+ packageManifest.devDependencies['react-compiler-runtime'],
+ '@happy-dom/global-registrator':
+ packageManifest.devDependencies['@happy-dom/global-registrator'],
+ },
+ }),
+ );
+ writeFileSync(
+ join(consumerDirectory, 'happydom.ts'),
+ "import {GlobalRegistrator} from '@happy-dom/global-registrator';\nGlobalRegistrator.register();\nglobalThis.IS_REACT_ACT_ENVIRONMENT = true;\n",
+ );
+ writeFileSync(
+ join(consumerDirectory, 'reactAct.ts'),
+ "import * as React from 'react';\nimport {act as legacyAct} from 'react-dom/test-utils';\nexport const act = React.act ?? legacyAct;\n",
+ );
+ cpSync(
+ join(fixturesDirectory, 'consumer.fixture.tsx'),
+ join(consumerDirectory, 'consumer.test.tsx'),
+ );
+ run(
+ 'npm',
+ [
+ 'install',
+ '--ignore-scripts',
+ '--no-audit',
+ '--no-fund',
+ '--package-lock=false',
+ ],
+ consumerDirectory,
+ );
+ console.log(
+ `\nChecking scale and lifecycle regressions without Compiler on React ${reactVersion}`,
+ );
+ run(
+ bunExecutable,
+ [
+ 'test',
+ '--preload',
+ './happydom.ts',
+ './compiler.scale.test.tsx',
+ './compiler.memory.test.tsx',
+ ],
+ consumerDirectory,
+ );
+ for (const {path, code} of compiledSources) {
+ writeFileSync(path, code);
+ }
+ console.log(
+ `\nChecking ${compilation.length} memoized components against packed React ${reactVersion} (compiler target ${target})`,
+ );
+ const testFiles = ['consumer.test.tsx'];
+ if (existsSync(join(consumerDirectory, 'compiler.qa.test.tsx'))) {
+ testFiles.push('compiler.qa.test.tsx');
+ }
+ testFiles.push('compiler.scale.test.tsx');
+ testFiles.push('compiler.memory.test.tsx');
+ run(
+ bunExecutable,
+ [
+ 'test',
+ '--preload',
+ './happydom.ts',
+ ...testFiles.map((path) => `./${path}`),
+ ],
+ consumerDirectory,
+ );
+ }
+} finally {
+ if (process.env.KEEP_COMPILER_TEMP === '1') {
+ console.log(`Kept compiled consumers at ${temporaryDirectory}`);
+ } else {
+ rmSync(temporaryDirectory, {recursive: true, force: true});
+ }
+}
diff --git a/packages/classy-store/tests/compiler/scale.store.ts b/packages/classy-store/tests/compiler/scale.store.ts
new file mode 100644
index 0000000..78d9068
--- /dev/null
+++ b/packages/classy-store/tests/compiler/scale.store.ts
@@ -0,0 +1,25 @@
+import {createClassyStore} from '@codebelt/classy-store';
+
+export class ScaleStore {
+ ignored = 0;
+ fields: {value: string}[];
+ rows: {title: string; count: number}[];
+
+ constructor(size: number) {
+ this.fields = Array.from({length: size}, (_, index) => ({
+ value: `Field ${index}`,
+ }));
+ this.rows = Array.from({length: size}, (_, index) => ({
+ title: `Row ${index}`,
+ count: index,
+ }));
+ }
+
+ setFieldValue(index: number, value: string) {
+ this.fields[index].value = value;
+ }
+}
+
+export function createScaleStore(size: number) {
+ return createClassyStore(new ScaleStore(size));
+}
diff --git a/packages/classy-store/tests/compiler/scale.types.ts b/packages/classy-store/tests/compiler/scale.types.ts
new file mode 100644
index 0000000..7e8504f
--- /dev/null
+++ b/packages/classy-store/tests/compiler/scale.types.ts
@@ -0,0 +1,8 @@
+import type {ScaleStore} from './scale.store';
+
+export type ScaleConsumerProps = {
+ store: ScaleStore;
+ index: number;
+ label: string;
+ onCommit: (index: number) => void;
+};
diff --git a/packages/classy-store/tests/compiler/tsconfig.json b/packages/classy-store/tests/compiler/tsconfig.json
new file mode 100644
index 0000000..7afd278
--- /dev/null
+++ b/packages/classy-store/tests/compiler/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "../.."
+ },
+ "include": ["**/*.ts", "**/*.tsx"]
+}
diff --git a/packages/classy-store/tests/memory/README.md b/packages/classy-store/tests/memory/README.md
new file mode 100644
index 0000000..d1acf6a
--- /dev/null
+++ b/packages/classy-store/tests/memory/README.md
@@ -0,0 +1,131 @@
+# React memory retention
+
+From the repository root, run:
+
+```sh
+bun run test:react:memory
+```
+
+Install Chromium first with `cd packages/classy-store && bunx playwright install
+chromium` if needed. The command builds the library, then runs a local browser
+harness against the built public entrypoints. Snapshots and `report.json` are
+written to `test-results/react-memory/`, which Git ignores. Raw snapshots can be
+large; use a separate output directory for each comparison you want to retain.
+
+The matrix runs five scenarios with primitive selectors, object selectors, and
+auto-tracking, with React Compiler off and on: **30 combinations**. Primitive
+selectors return the displayed text; object selectors return its containing row
+snapshot, exercising object identity and retained selections. Both builds use
+production React. The Compiler-on build compiles the consumer components; the
+library stays uncompiled. The compiler helper
+requires successful component compilation, memo slots, and a runtime import,
+and the report records which functions were compiled.
+
+## Scenarios
+
+| Scenario | Work performed each cycle | Expected ownership |
+| --- | --- | --- |
+| `mount` | Unmount the React root and mount a new scoped store. | Previous mounts release their stores and data. |
+| `reset` | Change the scoped component's key within the existing root. | Retired component instances release their stores and data. |
+| `replace` | Replace the collection while carrying its previous last row into the new first slot. | The live collection keeps that shared row; older generations must not form an accumulating history through tracking caches. |
+| `truncate` | Populate and read live row proxies, then set array length to zero. Refill with varying lengths. | Removed indices release child proxies; shrinking refills cannot hide stale high-index caches. |
+| `detached` | Keep one row proxy, replace its store's collection, then unmount. | The held row and its collection marker intentionally survive; its former store must be collectible. |
+
+The first four scenarios finish each measurement batch with a mounted consumer.
+The runner checks displayed values as well as memory. The detached scenario
+checks that the number of deliberately held row proxies equals completed cycles.
+Final cleanup unmounts everything and releases those explicit references.
+
+## Measurement and assertions
+
+Every combination opens a fresh page. The runner first holds one collection
+marker as a positive control, verifies that the heap parser finds it, releases
+it, and verifies its disappearance. It then captures heaps after one batch and
+after a second equal batch, followed by complete cleanup. Defaults are 25 cycles
+per batch and 64 rows per collection.
+
+React cleanup and browser tasks settle before collection. The Chrome DevTools
+Protocol requests garbage collection twice, across separate browser tasks,
+before each full V8 heap snapshot. Constructor names are preserved in the
+browser bundle. `MemoryStoreMarker` and `MemoryCollectionMarker` are opaque
+objects shared between mutable state and its snapshots, so their counts identify
+store and collection generations rather than copies made by snapshotting.
+
+The parser derives node offsets and type labels from snapshot metadata. It
+counts exact object instances, excluding identically named constructor functions,
+strings, and native nodes. Reports also include `TrackingReadMap` instance counts,
+node self-size totals, CDP heap usage, runtime/browser versions, and the build
+path and label. Self size is neither retained size nor browser process memory.
+
+Pass/fail checks use object counts:
+
+- Both measurement batches must expose at least one mounted store marker, with
+ at most two stores. Mounts and resets allow two collection generations for
+ React's current and alternate trees. Rolling replacements allow two for
+ selectors (the current and deliberately shared prior generation) and three
+ for auto-tracking (also the previous committed effect's reads).
+- Truncation must leave zero collection markers, including the initial data
+ snapshotted before the mutable array first became tracked.
+- Detached cases must retain no former store markers. Their collection markers
+ must equal the number of deliberately held rows until cleanup.
+- Full cleanup must leave zero store markers, collection markers, and tracking
+ read maps in every case.
+- Browser errors, failed positive controls, stale output, and failed compilation
+ also fail the run.
+
+Heap bytes and live-phase `TrackingReadMap` counts are diagnostic measurements,
+not CI thresholds. The parser does not present an ordinary edge walk as proof of strong
+retention: V8 WeakMap ephemeron edges need special interpretation. Load the raw
+`.heapsnapshot` files in Chrome DevTools Memory to inspect retaining paths when a
+marker count is unexpected.
+
+These are controlled Chromium experiments with the installed React version.
+They provide evidence for the tested ownership patterns, not proof that every
+application or browser is leak-free. Forced collection also does not describe
+normal browser GC timing or peak allocation rates.
+
+## Reproducing a comparison
+
+Preserve a baseline `dist` directory before rebuilding. Run the harness directly
+when targeting an existing build, so the root command does not rebuild it:
+
+```sh
+MEMORY_DIST=/absolute/path/to/baseline/dist \
+MEMORY_LABEL=baseline \
+MEMORY_OUTPUT=/tmp/classy-memory-baseline \
+MEMORY_OBSERVE_ONLY=1 \
+bun packages/classy-store/tests/memory/runMemory.ts
+
+MEMORY_LABEL=fixed \
+MEMORY_OUTPUT=/tmp/classy-memory-fixed \
+bun run test:react:memory
+```
+
+`MEMORY_OBSERVE_ONLY=1` records retention-bound failures without failing the
+process, which is useful for a known leaking baseline. It still checks controls,
+compilation, and rendered behavior. Do not use this option for CI validation.
+
+Optional settings are `MEMORY_CYCLES=25`, `MEMORY_SIZE=64`, and
+`MEMORY_SCENARIOS=mount,reset,replace,truncate,detached`. Cycle counts must be
+positive integers; size must be at least two. Compiler and subscription modes
+always run every variant. CI runs all 30 combinations with six cycles per batch
+and 24 rows. Keep build, browser, and workload settings identical when comparing
+before and after reports.
+
+The normal `bun run test` command also runs the synthetic heap-parser tests.
+Run them alone with:
+
+```sh
+bun test packages/classy-store/tests/memory/heapSnapshot.test.ts
+```
+
+`bun run typecheck` includes the complete memory harness.
+
+## Runtime requirement
+
+React auto-tracking now uses native **`WeakRef`** (ES2021) to publish committed
+reads without retaining obsolete snapshots from abandoned renders. Applications
+using auto-tracking need a runtime with `WeakRef` support. TypeScript compilation
+does not supply this runtime feature, and a strong-reference shim would defeat
+the memory guarantee. The harness uses the installed Playwright Chromium, which
+supports it. This requirement is separate from the supported React version range.
diff --git a/packages/classy-store/tests/memory/RESULTS.md b/packages/classy-store/tests/memory/RESULTS.md
new file mode 100644
index 0000000..b755695
--- /dev/null
+++ b/packages/classy-store/tests/memory/RESULTS.md
@@ -0,0 +1,83 @@
+# React memory retention results
+
+Measured on 2026-10-20 against the pre-memory-fix build at `583c532` and the
+working-tree fixes. Both builds used the same production consumer fixtures,
+React 19.3.1, React Compiler 1.0.0, Chromium 156.0.8078.4, Bun 1.3.14, and
+macOS/Darwin 25.6.0 arm64. Collections contained 64 rows; heap snapshots followed
+25 and 50 cycles. Each run covered all 30 scenario/mode/compiler combinations.
+
+These are **live constructor-instance counts after forced GC**, not heap-byte
+estimates. See [the methodology](./README.md) for controls and reproduction.
+Arrows below compare the two checkpoints within one run.
+
+| Workload / retained object | Before: 25 β 50 cycles | After: 25 β 50 cycles |
+| --- | ---: | ---: |
+| Repeated mounts: stores / collection generations | 1 / 1 β 1 / 1 | 1 / 1 β 1 / 1 |
+| Keyed resets: stores / collection generations | 1 / 1 β 1 / 1 | 1 / 1 β 1 / 1 |
+| Rolling replacement, auto-tracking: collection generations | 26 β 51 | 3 β 3 |
+| Rolling replacement, stable object selector: collection generations | 3 β 3 | 2 β 2 |
+| Rolling replacement, primitive selector, Compiler off: collection generations | 3 β 3 | 2 β 2 |
+| Rolling replacement, primitive selector, Compiler on: collection generations | 2 β 2 | 2 β 2 |
+| Truncation and shrinking refills: collection generations | 26 β 51 | 0 β 0 |
+| Held detached rows: former stores | 25 β 50 | 0 β 0 |
+
+Except where explicitly separated, results were identical with Compiler off
+and on, and across all applicable selector modes. Detached rows deliberately
+retain their own collection markers: 25 then 50 in both builds. Those references
+are released during cleanup. Every cleanup snapshot in both builds contained
+zero store markers, collection markers, and tracking read maps. The defects
+therefore required live owners or detached references; unmount-only tests would
+not have revealed them.
+
+## What changed
+
+- Removed child proxies no longer retain their former parents. Native array
+ truncation also evicts cached indices, including partial truncations rejected
+ by a non-configurable slot. Failed writes/deletes preserve valid connections.
+- Render read maps and their commit queues use weak references. A structurally
+ shared child can no longer anchor an accumulating chain of old snapshot roots.
+- Stable subscription and selector callbacks are constructed outside the hook's
+ render scope. This removes initial results retained through shared closure
+ contexts while preserving memoization and hydration behavior.
+- Transitioning a raw object into versioned snapshot tracking discards its
+ obsolete untracked clone. It also fixes stale snapshot reads when a fresh
+ store subsequently uses that raw object.
+
+Intermediate snapshots isolated the causes: weak read maps alone reduced
+tracked replacement from growing history to five generations; callback
+isolation reduced that to three. After the core truncation fix, one initial
+generation remained through `untrackedCache`; cache eviction reduced it to zero.
+Retainer inspection checked both ordinary strong paths and WeakMap key/value
+conditions rather than treating every heap edge as a strong reference.
+
+The remaining replacement generations have live owners: the current collection,
+the deliberately shared prior row, and in auto-tracked mode the prior committed
+React effect's reads. Counts stayed constant when the cycle count doubled.
+Prepared hydration stores intentionally retain their initial snapshot; this
+experiment uses client-only stores and does not attempt to remove that contract.
+
+## Regression and independent QA coverage
+
+- 495 source/parser tests passed, including deterministic removed-child,
+ failed-write, snapshot-cache, and committed-read regressions.
+- React 18.0.0, 18.3.1, and 19.3.1: 18 tests before compilation and 41 after
+ compilation per version, 177 successful executions. All 17 fixture components
+ produced actual Compiler memoization. Tests include StrictMode mounts, keyed
+ resets, store switches, collection replacement, truncation, and cleanup.
+- Independent QA repeated all 30 heap configurations with 24 rows and 6/12
+ cycles. It confirmed the same bounded counts, zero truncated generations,
+ zero detached former stores, and complete cleanup.
+- The existing Chromium/Firefox/WebKit suite passed 85 tests with eight existing
+ clipboard skips. The 16-case performance smoke preserved exact commit budgets
+ and real-input behavior. Timing samples from that smoke are not a performance
+ comparison; they ran alongside other validation.
+
+Raw local evidence from this run is under `/tmp/classy-memory-baseline-final`,
+`/tmp/classy-memory-fixed-complete`, and
+`/tmp/classy-memory-independent-complete-qa`. Each directory contains
+`report.json` and the corresponding `.heapsnapshot` files. These machine-local
+artifacts are not committed; use the documented commands to regenerate them.
+
+The evidence covers these ownership patterns in the tested Chromium runtime.
+It does not prove every application is leak-free or describe normal GC timing.
+Auto-tracked consumers now require native `WeakRef` support (ES2021).
diff --git a/packages/classy-store/tests/memory/client.tsx b/packages/classy-store/tests/memory/client.tsx
new file mode 100644
index 0000000..0292a83
--- /dev/null
+++ b/packages/classy-store/tests/memory/client.tsx
@@ -0,0 +1,114 @@
+import {flushSync} from 'react-dom';
+import {createRoot, type Root} from 'react-dom/client';
+import {MemoryScope} from './components/memoryScope/MemoryScope';
+import {type MemoryRow, type MemoryStore, makeRows} from './memory.store';
+import type {MemoryConfiguration} from './memory.types';
+
+let root: Root | undefined;
+let store: MemoryStore | undefined;
+let configuration: MemoryConfiguration;
+let generation = 0;
+let rawTail: MemoryRow | undefined;
+let heldRows: MemoryRow[] = [];
+let control: MemoryRow[] | undefined;
+
+function acceptStore(nextStore: MemoryStore | undefined): void {
+ store = nextStore;
+}
+
+function render(): void {
+ root ??= createRoot(document.getElementById('root') as HTMLElement);
+ root.render(
+ ,
+ );
+}
+
+function unmount(): void {
+ root?.unmount();
+ root = undefined;
+ store = undefined;
+ rawTail = undefined;
+}
+
+// Allow React's passive cleanup and browser jobs to finish before collecting.
+async function settle(): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ await new Promise((resolve) => setTimeout(resolve, 0));
+}
+
+window.memoryProbe = {
+ start(nextConfiguration) {
+ unmount();
+ heldRows = [];
+ configuration = nextConfiguration;
+ generation = 0;
+ flushSync(render);
+ },
+ async cycle(count) {
+ for (let cycle = 0; cycle < count; cycle += 1) {
+ generation += 1;
+ if (configuration.scenario === 'mount') {
+ unmount();
+ flushSync(render);
+ } else if (configuration.scenario === 'reset') {
+ flushSync(render);
+ } else if (configuration.scenario === 'detached') {
+ // Deliberately keep just a removed row, never its former store.
+ if (!store) flushSync(render);
+ if (!store) throw new Error('Missing mounted store');
+ heldRows.push(store.rows[0]);
+ flushSync(() => {
+ if (store) store.rows = [];
+ });
+ unmount();
+ } else if (configuration.scenario === 'replace') {
+ const rows = makeRows(configuration.size, generation);
+ // A rolling shared child exposes proxy-cache -> read-map history chains.
+ if (rawTail) rows[0] = rawTail;
+ rawTail = rows.at(-1);
+ flushSync(() => {
+ if (store) store.rows = rows;
+ });
+ } else {
+ flushSync(() => {
+ if (!store) throw new Error('Missing mounted store');
+ const rows = store.rows;
+ // Refill fewer slots each time so stale high-index caches cannot be
+ // hidden by overwriting every previous cached slot on the next turn.
+ const fillSize =
+ configuration.size - (generation % configuration.size);
+ rows.push(...makeRows(fillSize, generation));
+ // Access children through the live proxy to populate its lazy cache.
+ for (const row of rows) void row.text;
+ rows.length = 0;
+ });
+ }
+ await settle();
+ }
+ },
+ async cleanup() {
+ unmount();
+ heldRows = [];
+ control = undefined;
+ await settle();
+ },
+ holdControl() {
+ control = makeRows(1, -1);
+ },
+ releaseControl() {
+ control = undefined;
+ },
+ status() {
+ // Referencing control here keeps the positive control observable to the VM.
+ return {
+ text: document.querySelector('output')?.textContent ?? '',
+ cycles: generation,
+ heldRows: heldRows.length + (control?.length ?? 0),
+ };
+ },
+};
diff --git a/packages/classy-store/tests/memory/components/memoryObjectSelector/MemoryObjectSelector.tsx b/packages/classy-store/tests/memory/components/memoryObjectSelector/MemoryObjectSelector.tsx
new file mode 100644
index 0000000..530a25d
--- /dev/null
+++ b/packages/classy-store/tests/memory/components/memoryObjectSelector/MemoryObjectSelector.tsx
@@ -0,0 +1,14 @@
+import type {Snapshot} from '@codebelt/classy-store';
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {MemoryStore} from '../../memory.store';
+import type {MemoryConsumerProps} from '../../memory.types';
+
+// Stable identity exercises the hook's memoized selector getter across updates.
+function selectLastRow(state: Snapshot) {
+ return state.rows.at(-1);
+}
+
+export function MemoryObjectSelector({store}: MemoryConsumerProps) {
+ const row = useClassyStore(store, selectLastRow);
+ return {row?.text ?? 'empty'} ;
+}
diff --git a/packages/classy-store/tests/memory/components/memoryScope/MemoryScope.tsx b/packages/classy-store/tests/memory/components/memoryScope/MemoryScope.tsx
new file mode 100644
index 0000000..697cb15
--- /dev/null
+++ b/packages/classy-store/tests/memory/components/memoryScope/MemoryScope.tsx
@@ -0,0 +1,28 @@
+import {useLocalStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect} from 'react';
+import {MemoryStore} from '../../memory.store';
+import type {MemoryMode} from '../../memory.types';
+import {MemoryObjectSelector} from '../memoryObjectSelector/MemoryObjectSelector';
+import {MemorySelector} from '../memorySelector/MemorySelector';
+import {MemoryTracked} from '../memoryTracked/MemoryTracked';
+
+type Props = {
+ size: number;
+ mode: MemoryMode;
+ onStore: (store: MemoryStore | undefined) => void;
+};
+
+export function MemoryScope({size, mode, onStore}: Props) {
+ const store = useLocalStore(() => new MemoryStore(size));
+ useLayoutEffect(() => {
+ onStore(store);
+ return () => onStore(undefined);
+ }, [store, onStore]);
+ return mode === 'selector' ? (
+
+ ) : mode === 'object-selector' ? (
+
+ ) : (
+
+ );
+}
diff --git a/packages/classy-store/tests/memory/components/memorySelector/MemorySelector.tsx b/packages/classy-store/tests/memory/components/memorySelector/MemorySelector.tsx
new file mode 100644
index 0000000..b825532
--- /dev/null
+++ b/packages/classy-store/tests/memory/components/memorySelector/MemorySelector.tsx
@@ -0,0 +1,10 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {MemoryConsumerProps} from '../../memory.types';
+
+export function MemorySelector({store}: MemoryConsumerProps) {
+ const text = useClassyStore(
+ store,
+ (state) => state.rows.at(-1)?.text ?? 'empty',
+ );
+ return {text} ;
+}
diff --git a/packages/classy-store/tests/memory/components/memoryTracked/MemoryTracked.tsx b/packages/classy-store/tests/memory/components/memoryTracked/MemoryTracked.tsx
new file mode 100644
index 0000000..8bbc88d
--- /dev/null
+++ b/packages/classy-store/tests/memory/components/memoryTracked/MemoryTracked.tsx
@@ -0,0 +1,7 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {MemoryConsumerProps} from '../../memory.types';
+
+export function MemoryTracked({store}: MemoryConsumerProps) {
+ const state = useClassyStore(store);
+ return {state.rows.at(-1)?.text ?? 'empty'} ;
+}
diff --git a/packages/classy-store/tests/memory/heapSnapshot.test.ts b/packages/classy-store/tests/memory/heapSnapshot.test.ts
new file mode 100644
index 0000000..e3b4817
--- /dev/null
+++ b/packages/classy-store/tests/memory/heapSnapshot.test.ts
@@ -0,0 +1,94 @@
+import {describe, expect, test} from 'bun:test';
+import {summarizeHeapSnapshot} from './heapSnapshot';
+
+describe('summarizeHeapSnapshot', () => {
+ test('counts exact object instances while excluding constructor and string names', () => {
+ const json = JSON.stringify({
+ snapshot: {
+ meta: {
+ node_fields: ['type', 'name', 'id', 'self_size', 'edge_count'],
+ node_types: [
+ ['hidden', 'array', 'string', 'object', 'closure', 'native'],
+ 'string',
+ 'number',
+ 'number',
+ 'number',
+ ],
+ },
+ },
+ strings: [
+ '',
+ 'MemoryStoreMarker',
+ 'MemoryCollectionMarker',
+ 'TrackingReadMap',
+ 'MemoryStoreMarkerSuffix',
+ ],
+ nodes: [
+ 3, 1, 1, 32, 0, 3, 1, 2, 32, 0, 3, 2, 3, 40, 0, 3, 3, 4, 48, 0, 4, 1, 5,
+ 64, 0, 2, 2, 6, 24, 0, 1, 3, 7, 32, 0, 3, 4, 8, 16, 0, 5, 1, 9, 128, 0,
+ ],
+ });
+
+ expect(summarizeHeapSnapshot(json)).toEqual({
+ counts: {
+ MemoryStoreMarker: 2,
+ MemoryCollectionMarker: 1,
+ TrackingReadMap: 1,
+ },
+ totalSelfSize: 416,
+ });
+ });
+
+ test('uses schema offsets and type labels instead of a fixed V8 layout', () => {
+ const json = JSON.stringify({
+ snapshot: {
+ meta: {
+ node_fields: ['self_size', 'id', 'name', 'type'],
+ node_types: ['number', 'number', 'string', ['object', 'string']],
+ },
+ },
+ strings: ['TrackingReadMap', 'MemoryCollectionMarker'],
+ nodes: [48, 1, 0, 0, 40, 2, 1, 0, 24, 3, 1, 1],
+ });
+
+ expect(summarizeHeapSnapshot(json)).toEqual({
+ counts: {
+ MemoryStoreMarker: 0,
+ MemoryCollectionMarker: 1,
+ TrackingReadMap: 1,
+ },
+ totalSelfSize: 112,
+ });
+ });
+
+ test.each([
+ {
+ description: 'missing size metadata',
+ nodeFields: ['type', 'name'],
+ nodeTypes: [['object'], 'string'],
+ nodes: [0, 0],
+ },
+ {
+ description: 'truncated node record',
+ nodeFields: ['type', 'name', 'self_size'],
+ nodeTypes: [['object'], 'string', 'number'],
+ nodes: [0, 0],
+ },
+ {
+ description: 'missing object type',
+ nodeFields: ['type', 'name', 'self_size'],
+ nodeTypes: [['string'], 'string', 'number'],
+ nodes: [0, 0, 32],
+ },
+ ])('rejects $description', ({nodeFields, nodeTypes, nodes}) => {
+ const json = JSON.stringify({
+ snapshot: {meta: {node_fields: nodeFields, node_types: nodeTypes}},
+ strings: ['MemoryStoreMarker'],
+ nodes,
+ });
+
+ expect(() => summarizeHeapSnapshot(json)).toThrow(
+ 'Unsupported V8 heap snapshot node schema',
+ );
+ });
+});
diff --git a/packages/classy-store/tests/memory/heapSnapshot.ts b/packages/classy-store/tests/memory/heapSnapshot.ts
new file mode 100644
index 0000000..3644212
--- /dev/null
+++ b/packages/classy-store/tests/memory/heapSnapshot.ts
@@ -0,0 +1,52 @@
+type HeapSnapshot = {
+ snapshot: {
+ meta: {
+ node_fields: string[];
+ node_types: (string | string[])[];
+ };
+ };
+ nodes: number[];
+ strings: string[];
+};
+
+/**
+ * Count live marker instances in a V8 heap snapshot after the harness forces GC.
+ * Self size is the sum of node sizes, not retained size or browser process RSS.
+ * Edges are deliberately excluded: WeakMap ephemerons need special treatment
+ * and cannot be interpreted as ordinary strong references in a retainer walk.
+ */
+export function summarizeHeapSnapshot(json: string): {
+ counts: Record;
+ totalSelfSize: number;
+} {
+ const heap = JSON.parse(json) as HeapSnapshot;
+ const fields = heap.snapshot.meta.node_fields;
+ const typeOffset = fields.indexOf('type');
+ const nameOffset = fields.indexOf('name');
+ const sizeOffset = fields.indexOf('self_size');
+ const nodeTypes = heap.snapshot.meta.node_types[typeOffset];
+ if (
+ typeOffset < 0 ||
+ nameOffset < 0 ||
+ sizeOffset < 0 ||
+ !Array.isArray(nodeTypes) ||
+ !nodeTypes.includes('object') ||
+ heap.nodes.length % fields.length !== 0
+ ) {
+ throw new Error('Unsupported V8 heap snapshot node schema');
+ }
+
+ const counts: Record = {
+ MemoryStoreMarker: 0,
+ MemoryCollectionMarker: 0,
+ TrackingReadMap: 0,
+ };
+ let totalSelfSize = 0;
+ for (let offset = 0; offset < heap.nodes.length; offset += fields.length) {
+ totalSelfSize += heap.nodes[offset + sizeOffset];
+ if (nodeTypes[heap.nodes[offset + typeOffset]] !== 'object') continue;
+ const name = heap.strings[heap.nodes[offset + nameOffset]];
+ if (Object.hasOwn(counts, name)) counts[name] += 1;
+ }
+ return {counts, totalSelfSize};
+}
diff --git a/packages/classy-store/tests/memory/memory.store.ts b/packages/classy-store/tests/memory/memory.store.ts
new file mode 100644
index 0000000..0b96f3d
--- /dev/null
+++ b/packages/classy-store/tests/memory/memory.store.ts
@@ -0,0 +1,31 @@
+// Opaque markers are shared by mutable data and every snapshot of that data.
+// They let heap snapshots count generations without counting snapshot copies.
+export class MemoryStoreMarker {
+ payload = Array.from({length: 256}, (_, index) => index);
+}
+
+export class MemoryCollectionMarker {
+ payload = Array.from({length: 1024}, (_, index) => index);
+}
+
+export type MemoryRow = {
+ text: string;
+ marker: MemoryCollectionMarker;
+};
+
+export function makeRows(size: number, generation: number): MemoryRow[] {
+ const marker = new MemoryCollectionMarker();
+ return Array.from({length: size}, (_, index) => ({
+ text: `${generation}:${index}`,
+ marker,
+ }));
+}
+
+export class MemoryStore {
+ marker = new MemoryStoreMarker();
+ rows: MemoryRow[];
+
+ constructor(size: number) {
+ this.rows = makeRows(size, 0);
+ }
+}
diff --git a/packages/classy-store/tests/memory/memory.types.ts b/packages/classy-store/tests/memory/memory.types.ts
new file mode 100644
index 0000000..62f60b2
--- /dev/null
+++ b/packages/classy-store/tests/memory/memory.types.ts
@@ -0,0 +1,29 @@
+import type {MemoryStore} from './memory.store';
+
+export type MemoryMode = 'selector' | 'object-selector' | 'tracked';
+export type MemoryScenario =
+ | 'mount'
+ | 'reset'
+ | 'replace'
+ | 'truncate'
+ | 'detached';
+export type MemoryConfiguration = {
+ mode: MemoryMode;
+ scenario: MemoryScenario;
+ size: number;
+};
+export type MemoryConsumerProps = {store: MemoryStore};
+export type MemoryProbe = {
+ start: (configuration: MemoryConfiguration) => void;
+ cycle: (count: number) => Promise;
+ cleanup: () => Promise;
+ holdControl: () => void;
+ releaseControl: () => void;
+ status: () => {text: string; cycles: number; heldRows: number};
+};
+
+declare global {
+ interface Window {
+ memoryProbe: MemoryProbe;
+ }
+}
diff --git a/packages/classy-store/tests/memory/runMemory.ts b/packages/classy-store/tests/memory/runMemory.ts
new file mode 100644
index 0000000..a91c888
--- /dev/null
+++ b/packages/classy-store/tests/memory/runMemory.ts
@@ -0,0 +1,309 @@
+import assert from 'node:assert/strict';
+import {mkdir} from 'node:fs/promises';
+import {arch, platform, release} from 'node:os';
+import {basename, dirname, resolve} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import {type CDPSession, chromium, type Page} from '@playwright/test';
+import {compileConsumer} from '../compiler/compileConsumer.mjs';
+import {summarizeHeapSnapshot} from './heapSnapshot';
+import type {MemoryMode, MemoryScenario} from './memory.types';
+
+const directory = dirname(fileURLToPath(import.meta.url));
+const manifest = await Bun.file(
+ resolve(directory, '../../package.json'),
+).json();
+const dist = resolve(
+ process.env.MEMORY_DIST ?? resolve(directory, '../../dist'),
+);
+const output = resolve(
+ process.env.MEMORY_OUTPUT ??
+ resolve(directory, '../../../../test-results/react-memory'),
+);
+const cycles = Number(process.env.MEMORY_CYCLES ?? 25);
+const size = Number(process.env.MEMORY_SIZE ?? 64);
+const scenarios = (
+ process.env.MEMORY_SCENARIOS ?? 'mount,reset,replace,truncate,detached'
+).split(',') as MemoryScenario[];
+const observeOnly = process.env.MEMORY_OBSERVE_ONLY === '1';
+assert(Number.isInteger(cycles) && cycles > 0);
+assert(Number.isInteger(size) && size >= 2);
+assert(
+ scenarios.every((scenario) =>
+ ['mount', 'reset', 'replace', 'truncate', 'detached'].includes(scenario),
+ ),
+);
+await mkdir(output, {recursive: true});
+
+const compilation: unknown[] = [];
+const scripts = new Map();
+for (const compiler of ['off', 'on']) {
+ const bundle = await Bun.build({
+ entrypoints: [resolve(directory, 'client.tsx')],
+ target: 'browser',
+ format: 'iife',
+ // Preserve constructor names used by the heap analyzer.
+ minify: {whitespace: true, syntax: true, identifiers: false},
+ define: {'process.env.NODE_ENV': '"production"'},
+ plugins: [
+ {
+ name: 'memory-consumer',
+ setup(build) {
+ build.onResolve(
+ {filter: /^(?:react(?:\/.*)?|react-dom(?:\/.*)?|proxy-compare)$/},
+ ({path}) => ({path: Bun.resolveSync(path, directory)}),
+ );
+ build.onResolve(
+ {filter: /^@codebelt\/classy-store(?:\/react)?$/},
+ ({path}) => ({
+ path: resolve(
+ dist,
+ path.endsWith('/react')
+ ? 'frameworks/react/react.mjs'
+ : 'index.mjs',
+ ),
+ }),
+ );
+ if (compiler === 'on')
+ build.onLoad(
+ {filter: /\/memory\/components\/.*\.tsx$/},
+ async ({path}) => {
+ const result = compileConsumer(
+ await Bun.file(path).text(),
+ path,
+ '19',
+ [basename(path, '.tsx')],
+ );
+ compilation.push({
+ filename: basename(path),
+ functions: result.compiledFunctions,
+ });
+ return {contents: result.code, loader: 'tsx'};
+ },
+ );
+ },
+ },
+ ],
+ });
+ if (!bundle.success)
+ throw new AggregateError(bundle.logs, 'Memory consumer build failed');
+ scripts.set(`/${compiler}.js`, await bundle.outputs[0].text());
+}
+const server = Bun.serve({
+ hostname: '127.0.0.1',
+ port: 0,
+ fetch(request) {
+ const url = new URL(request.url);
+ const script = scripts.get(url.pathname);
+ if (script)
+ return new Response(script, {
+ headers: {'Content-Type': 'text/javascript'},
+ });
+ const compiler = url.searchParams.get('compiler') === 'on' ? 'on' : 'off';
+ return new Response(
+ `React memory probe
`,
+ {headers: {'Content-Type': 'text/html'}},
+ );
+ },
+});
+
+async function captureHeap(page: Page, session: CDPSession, path: string) {
+ await page.evaluate(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ });
+ await session.send('HeapProfiler.collectGarbage');
+ await page.evaluate(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ });
+ await session.send('HeapProfiler.collectGarbage');
+ const chunks: string[] = [];
+ const onChunk = ({chunk}: {chunk: string}) => chunks.push(chunk);
+ session.on('HeapProfiler.addHeapSnapshotChunk', onChunk);
+ try {
+ await session.send('HeapProfiler.takeHeapSnapshot', {
+ reportProgress: false,
+ });
+ } finally {
+ session.off('HeapProfiler.addHeapSnapshotChunk', onChunk);
+ }
+ const json = chunks.join('');
+ await Bun.write(path, json);
+ return {
+ file: basename(path),
+ ...summarizeHeapSnapshot(json),
+ heapUsage: await session.send('Runtime.getHeapUsage'),
+ };
+}
+
+const browser = await chromium.launch();
+const results: unknown[] = [];
+const failures: string[] = [];
+try {
+ for (const scenario of scenarios)
+ for (const mode of [
+ 'selector',
+ 'object-selector',
+ 'tracked',
+ ] as MemoryMode[])
+ for (const compiler of ['off', 'on']) {
+ const name = `${scenario}-${mode}-${compiler}`;
+ const page = await browser.newPage();
+ const errors: string[] = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ page.on('console', (message) => {
+ if (message.type() === 'error') errors.push(message.text());
+ });
+ await page.goto(`${server.url}?compiler=${compiler}`);
+ await page
+ .waitForFunction(() => Boolean(window.memoryProbe))
+ .catch((error) => {
+ throw new AggregateError(
+ [error, ...errors],
+ `Could not load ${name}`,
+ );
+ });
+ const session = await page.context().newCDPSession(page);
+ await session.send('HeapProfiler.enable');
+ const snapshots: Record<
+ string,
+ Awaited>
+ > = {};
+ // Positive control: parser must see an intentionally retained generation.
+ await page.evaluate(() => window.memoryProbe.holdControl());
+ snapshots.control = await captureHeap(
+ page,
+ session,
+ resolve(output, `${name}-control.heapsnapshot`),
+ );
+ assert.equal(snapshots.control.counts.MemoryCollectionMarker, 1);
+ await page.evaluate(() => window.memoryProbe.releaseControl());
+ snapshots.empty = await captureHeap(
+ page,
+ session,
+ resolve(output, `${name}-empty.heapsnapshot`),
+ );
+ assert.equal(snapshots.empty.counts.MemoryCollectionMarker, 0);
+ assert.equal(snapshots.empty.counts.MemoryStoreMarker, 0);
+ await page.evaluate(
+ (configuration) => window.memoryProbe.start(configuration),
+ {scenario, mode, size},
+ );
+ for (const checkpoint of ['first', 'second']) {
+ await page.evaluate(
+ (count) => window.memoryProbe.cycle(count),
+ cycles,
+ );
+ const status = await page.evaluate(() => window.memoryProbe.status());
+ assert.equal(
+ status.cycles,
+ checkpoint === 'first' ? cycles : cycles * 2,
+ );
+ if (scenario === 'detached')
+ assert.equal(status.heldRows, status.cycles);
+ else {
+ const expectedText =
+ scenario === 'truncate'
+ ? 'empty'
+ : `${scenario === 'replace' ? status.cycles : 0}:${size - 1}`;
+ assert.equal(
+ status.text,
+ expectedText,
+ 'Mounted consumer must show current data',
+ );
+ }
+ snapshots[checkpoint] = await captureHeap(
+ page,
+ session,
+ resolve(output, `${name}-${checkpoint}.heapsnapshot`),
+ );
+ }
+ await page.evaluate(() => window.memoryProbe.cleanup());
+ snapshots.cleanup = await captureHeap(
+ page,
+ session,
+ resolve(output, `${name}-cleanup.heapsnapshot`),
+ );
+ assert.deepEqual(errors, []);
+ // Object-count bounds account for current/alternate React trees, not bytes.
+ // Detached rows are deliberately held; their former stores must disappear.
+ // Rolling replacements keep the current and previous raw generation.
+ // Auto-tracking can also retain the previous committed effect's reads.
+ const collectionLimit =
+ scenario === 'truncate'
+ ? 0
+ : scenario === 'replace' && mode === 'tracked'
+ ? 3
+ : 2;
+ for (const checkpoint of ['first', 'second']) {
+ const counts = snapshots[checkpoint].counts;
+ if (scenario !== 'detached')
+ assert(
+ counts.MemoryStoreMarker >= 1,
+ 'Mounted store marker must be observable',
+ );
+ if (counts.MemoryStoreMarker > (scenario === 'detached' ? 0 : 2))
+ failures.push(
+ `${name}/${checkpoint}: retained ${counts.MemoryStoreMarker} stores`,
+ );
+ if (scenario === 'detached') {
+ const heldCount = checkpoint === 'first' ? cycles : cycles * 2;
+ assert.equal(
+ counts.MemoryCollectionMarker,
+ heldCount,
+ 'Each deliberately held row must retain only its own generation',
+ );
+ }
+ if (
+ scenario !== 'detached' &&
+ counts.MemoryCollectionMarker > collectionLimit
+ )
+ failures.push(
+ `${name}/${checkpoint}: retained ${counts.MemoryCollectionMarker} collection generations (limit ${collectionLimit})`,
+ );
+ }
+ if (
+ snapshots.cleanup.counts.MemoryStoreMarker ||
+ snapshots.cleanup.counts.MemoryCollectionMarker ||
+ snapshots.cleanup.counts.TrackingReadMap
+ )
+ failures.push(`${name}: markers or read maps survive full cleanup`);
+ const second = snapshots.second.counts;
+ results.push({scenario, mode, compiler, snapshots});
+ console.log(
+ `${name}: stores ${snapshots.first.counts.MemoryStoreMarker}β${second.MemoryStoreMarker}; collections ${snapshots.first.counts.MemoryCollectionMarker}β${second.MemoryCollectionMarker}; cleanup ${JSON.stringify(snapshots.cleanup.counts)}`,
+ );
+ await session.detach();
+ await page.close();
+ }
+ await Bun.write(
+ resolve(output, 'report.json'),
+ JSON.stringify(
+ {
+ metadata: {
+ label: process.env.MEMORY_LABEL ?? 'working-tree',
+ createdAt: new Date().toISOString(),
+ react: manifest.devDependencies.react,
+ compiler: manifest.devDependencies['babel-plugin-react-compiler'],
+ browser: browser.version(),
+ bun: Bun.version,
+ os: `${platform()} ${release()} ${arch()}`,
+ build: 'production',
+ dist,
+ cycles,
+ size,
+ observeOnly,
+ },
+ compilation,
+ failures,
+ results,
+ },
+ null,
+ 2,
+ ),
+ );
+ console.log(`Heap snapshots and report saved to ${output}`);
+ if (!observeOnly)
+ assert.deepEqual(failures, [], 'Retained object bounds exceeded');
+} finally {
+ await browser.close();
+ server.stop(true);
+}
diff --git a/packages/classy-store/tests/memory/tsconfig.json b/packages/classy-store/tests/memory/tsconfig.json
new file mode 100644
index 0000000..8ad0b3c
--- /dev/null
+++ b/packages/classy-store/tests/memory/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {"rootDir": "../.."},
+ "include": ["**/*.ts", "**/*.tsx"]
+}
diff --git a/packages/classy-store/tests/next/README.md b/packages/classy-store/tests/next/README.md
new file mode 100644
index 0000000..31fb40f
--- /dev/null
+++ b/packages/classy-store/tests/next/README.md
@@ -0,0 +1,101 @@
+# Packed Next.js App Router integration
+
+From the repository root, run:
+
+```sh
+bun run test:react:next
+```
+
+Install Chromium first with `cd packages/classy-store && bunx playwright install
+chromium` if needed. The root command builds the library, creates an npm tarball,
+installs it into a separate consumer directory, and runs production Next.js builds
+and Chromium tests with React Compiler off and on. This uses the published package
+entrypoints, without source aliases, workspace links, or `transpilePackages`.
+
+The consumer pins Next.js 16.4.2 and uses the React, React DOM, and Compiler versions
+declared in the library's development dependencies. It uses Turbopack and explicitly
+sets `cacheComponents: false`. The tested navigation contract therefore includes
+route unmounts and resets. React Activity preservation with Cache Components enabled
+is a separate integration mode, not an assumption made by these tests.
+
+Next App Router can supply its own React runtime. The compiler-evidence JSON
+records the actual browser `React.version` separately from the installed package
+versions in `report.json`. In the current Next.js 16.4.2 matrix that runtime is
+`19.3.0-canary-278794d7-20261002`, while the installed React package is 19.3.1.
+
+## Checks
+
+- **Packed boundaries:** both ESM and CommonJS React hook entrypoints must retain
+ their leading `use client` directive. The core, collections, utilities, and
+ `@codebelt/classy-store/react/server` entrypoints stay server safe and execute
+ under Node's `react-server` condition. Normal SSR also checks that the server and
+ React entrypoints share the same hydration preparation function and initial
+ snapshot registry.
+- **Actual Compiler output:** the browser captures the production selector and
+ auto-tracked consumer function bodies. Both must allocate a positive number of
+ Compiler memo-cache slots and read and write those slots when Compiler is enabled;
+ neither may allocate them when it is disabled. The bodies and extracted slot
+ evidence are saved as JSON and attached to the Playwright result, so setting a
+ config flag without transforming consumers cannot pass this check. A sentinel
+ literal is not required: input-dependent memoization can use slot comparisons
+ without including that literal in the component body.
+- **Request isolation:** six overlapping requests use distinct seeds and verify
+ their server store snapshots, collection contents, and server-rendered selector
+ and auto-tracked values. A deliberate macrotask between mutation and snapshot
+ makes request overlap observable.
+- **Controlled inputs:** real browser typing updates selectors and auto-tracked
+ consumers synchronously, including insertion into the middle of an input with
+ the caret preserved.
+- **Navigation:** real links, browser back/forward, and `router.refresh()` preserve
+ shared layout state while route stores reset on replacement. A changed server
+ request nonce proves refresh has merged the new payload. A document sentinel
+ proves navigation did not replace the document. Leaving the layout disposes its
+ persistence subscriptions; mutating the retired stores must cause no writes.
+- **Streaming:** a unique server gate holds an async Server Component. The raw
+ response must deliver the fallback before the gate is released, then deliver
+ the server-rendered fields. This checks response ordering, not just final HTML.
+- **Delayed hydration:** a second browser gate delays the streamed client subtree
+ independently of the server response. Parent persistence must restore saved
+ state while the child still displays its server values and remains unhydrated.
+ After release, selector, auto-tracked output, and input update to saved data;
+ their exact original DOM nodes must survive. Subsequent typing also works.
+
+Every browser test fails on page exceptions or console errors, including recoverable
+hydration failures. Server gates have a bounded fallback timeout; the network test
+deadline is shorter, so waiting for that timeout cannot pass the streaming check.
+
+## Outputs and focused runs
+
+Logs, package-boundary results, Playwright diagnostics, and the final report are
+written to `test-results/next/` by default. Build output and browser diagnostics
+are separated by Compiler mode. CI runs each mode in its own job and uploads these
+artifacts, including failure traces.
+
+```sh
+# One Compiler mode, retaining the generated consumer for inspection.
+NEXT_TEST_MODES=on KEEP_NEXT_TEMP=1 bun run test:react:next
+
+# Reuse a dedicated consumer directory and choose where reports are written.
+NEXT_TEST_DIRECTORY=/tmp/classy-next-consumer \
+NEXT_TEST_OUTPUT=/tmp/classy-next-results \
+bun run test:react:next
+```
+
+`NEXT_TEST_MODES` defaults to `off,on`. `NEXT_TEST_DIRECTORY` must be a dedicated
+scratch directory: the runner copies fixture files and installs dependencies there.
+Without that setting the runner creates a temporary directory, deletes it after
+success, and retains it after failure. `KEEP_NEXT_TEMP=1` also retains successful
+temporary consumers. `NEXT_TEST_OUTPUT` changes the report directory.
+
+To rerun browser tests against an already running production consumer:
+
+```sh
+NEXT_TEST_URL=http://127.0.0.1:3000 NEXT_TEST_COMPILER=on \
+packages/classy-store/node_modules/.bin/playwright test \
+ --config packages/classy-store/tests/next/playwright.config.ts
+```
+
+Both variables are required. `NEXT_TEST_COMPILER` must match the production build.
+The runner provides them automatically. The outer test harness is included in
+`bun run typecheck`; the isolated Next.js production build typechecks the fixture
+against the installed package declarations.
diff --git a/packages/classy-store/tests/next/checkBoundaries.mjs b/packages/classy-store/tests/next/checkBoundaries.mjs
new file mode 100644
index 0000000..6cfd869
--- /dev/null
+++ b/packages/classy-store/tests/next/checkBoundaries.mjs
@@ -0,0 +1,165 @@
+import assert from 'node:assert/strict';
+import {spawnSync} from 'node:child_process';
+import {existsSync, readFileSync} from 'node:fs';
+import {dirname, join, resolve} from 'node:path';
+import {fileURLToPath} from 'node:url';
+
+const fixtureDirectory = dirname(fileURLToPath(import.meta.url));
+const consumerDirectory = resolve(
+ process.argv[2] ?? join(fixtureDirectory, '../..'),
+);
+const installedPackageDirectory = join(
+ consumerDirectory,
+ 'node_modules/@codebelt/classy-store',
+);
+const packageDirectory = existsSync(installedPackageDirectory)
+ ? installedPackageDirectory
+ : consumerDirectory;
+const manifest = JSON.parse(
+ readFileSync(join(packageDirectory, 'package.json'), 'utf8'),
+);
+const failures = [];
+const checks = [];
+
+function check(name, action) {
+ try {
+ action();
+ checks.push(name);
+ } catch (error) {
+ failures.push({name, error: String(error)});
+ }
+}
+
+function hasClientDirective(source) {
+ // A directive must occur before executable code; a string elsewhere in a
+ // bundled module (including a comment) does not establish an RSC boundary.
+ let remaining = source;
+ for (;;) {
+ remaining = remaining.replace(
+ /^(?:\s+|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)+/,
+ '',
+ );
+ const directive = remaining.match(/^(['"])([^'"\n]*)\1\s*;/);
+ if (!directive) return false;
+ if (directive[2] === 'use client') return true;
+ remaining = remaining.slice(directive[0].length);
+ }
+}
+
+for (const mode of ['import', 'require']) {
+ for (const entry of ['.', './collections', './utils', './react/server']) {
+ check(`${mode} ${entry} stays server safe`, () => {
+ const target = manifest.exports[entry]?.[mode]?.default;
+ assert.equal(typeof target, 'string', `Missing ${entry} ${mode} export`);
+ const source = readFileSync(join(packageDirectory, target), 'utf8');
+ assert.equal(hasClientDirective(source), false);
+ });
+ }
+ check(`${mode} ./react preserves its client boundary`, () => {
+ const target = manifest.exports['./react'][mode].default;
+ const source = readFileSync(join(packageDirectory, target), 'utf8');
+ assert.equal(hasClientDirective(source), true);
+ });
+}
+
+function runProbe(mode, serverCondition) {
+ const load = (name, packageName) =>
+ mode === 'import'
+ ? `import * as ${name} from '${packageName}';`
+ : `const ${name} = require('${packageName}');`;
+ const source = [
+ mode === 'import'
+ ? "import assert from 'node:assert/strict';"
+ : "const assert = require('node:assert/strict');",
+ load('core', '@codebelt/classy-store'),
+ load('collections', '@codebelt/classy-store/collections'),
+ load('utils', '@codebelt/classy-store/utils'),
+ load('server', '@codebelt/classy-store/react/server'),
+ ...(serverCondition
+ ? []
+ : [
+ load('client', '@codebelt/classy-store/react'),
+ load('React', 'react'),
+ load('ReactDOMServer', 'react-dom/server'),
+ ]),
+ `
+ async function verify() {
+ const first = core.createClassyStore({
+ count: 1,
+ items: collections.reactiveMap([['first', 1]]),
+ tags: collections.reactiveSet(['first']),
+ });
+ const second = core.createClassyStore({count: 10});
+ assert.equal(server.prepareStoreForHydration(first), first);
+ assert.equal(server.prepareStoreForHydration(second), second);
+ assert.throws(() => server.prepareStoreForHydration({count: 0}));
+ let calls = 0;
+ const stop = utils.subscribeKey(first, 'count', () => { calls++; });
+ const history = utils.withHistory(first);
+ first.count = 2;
+ first.items.set('second', 2);
+ first.tags.add('second');
+ await Promise.resolve();
+ stop();
+ history.dispose();
+ utils.devtools(first)();
+ assert.equal(calls, 1);
+ assert.equal(core.snapshot(first).count, 2);
+ assert.equal(core.snapshot(second).count, 10);
+ assert.equal(first.items.get('second'), 2);
+ assert.equal(first.tags.has('second'), true);
+ ${
+ serverCondition
+ ? ''
+ : `
+ assert.equal(server.prepareStoreForHydration, client.prepareStoreForHydration);
+ for (const prepare of [server.prepareStoreForHydration, client.prepareStoreForHydration]) {
+ for (const tracked of [false, true]) {
+ const store = core.createClassyStore({count: 7});
+ prepare(store);
+ store.count = 9;
+ prepare(store);
+ function Probe() {
+ const value = tracked
+ ? client.useClassyStore(store).count
+ : client.useClassyStore(store, state => state.count);
+ return React.createElement('output', null, value);
+ }
+ assert.equal(ReactDOMServer.renderToString(React.createElement(Probe)), '7 ');
+ }
+ }
+ `
+ }
+ }
+ verify().catch(error => { console.error(error); process.exitCode = 1; });
+ `,
+ ].join('\n');
+ const result = spawnSync(
+ process.execPath,
+ [
+ ...(serverCondition ? ['--conditions=react-server'] : []),
+ ...(mode === 'import' ? ['--input-type=module'] : []),
+ '--eval',
+ source,
+ ],
+ {
+ cwd: consumerDirectory,
+ encoding: 'utf8',
+ env: {...process.env, NODE_ENV: 'production'},
+ },
+ );
+ assert.equal(result.error, undefined);
+ assert.equal(result.status, 0, result.stderr || result.stdout);
+}
+
+for (const mode of ['import', 'require']) {
+ check(`${mode} executes server-safe APIs under react-server`, () =>
+ runProbe(mode, true),
+ );
+ check(`${mode} shares hydration preparation with React hooks`, () =>
+ runProbe(mode, false),
+ );
+}
+
+console.log(JSON.stringify({checks, failures}, null, 2));
+if (failures.length) process.exitCode = 1;
diff --git a/packages/classy-store/tests/next/compilerEvidence.ts b/packages/classy-store/tests/next/compilerEvidence.ts
new file mode 100644
index 0000000..5de0c87
--- /dev/null
+++ b/packages/classy-store/tests/next/compilerEvidence.ts
@@ -0,0 +1,26 @@
+import type {CompilerEvidence} from './next.types';
+
+export function analyzeCompilerEvidence(source: string): CompilerEvidence {
+ // Next's production minifier preserves the public `c` export from
+ // react/compiler-runtime while shortening its imported namespace name.
+ const allocation = source.match(
+ /(? [
+ ...new Set([...source.matchAll(pattern)].map((match) => Number(match[1]))),
+ ];
+ return {
+ cacheVariable,
+ slotCount: Number(allocation[2]),
+ readSlots: slots(reads),
+ writtenSlots: slots(writes),
+ };
+}
diff --git a/packages/classy-store/tests/next/fixture/app/api/stream-gate/route.ts b/packages/classy-store/tests/next/fixture/app/api/stream-gate/route.ts
new file mode 100644
index 0000000..bf95017
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/app/api/stream-gate/route.ts
@@ -0,0 +1,10 @@
+import {releaseServerGate} from '../../../utils/serverGate/serverGate';
+
+export async function POST(request: Request) {
+ const body = (await request.json()) as {ticket?: unknown};
+ if (typeof body.ticket !== 'string' || body.ticket.length > 128) {
+ return Response.json({error: 'A ticket is required'}, {status: 400});
+ }
+ releaseServerGate(body.ticket);
+ return Response.json({released: true});
+}
diff --git a/packages/classy-store/tests/next/fixture/app/layout.tsx b/packages/classy-store/tests/next/fixture/app/layout.tsx
new file mode 100644
index 0000000..6ce0d8d
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/app/layout.tsx
@@ -0,0 +1,13 @@
+import type {ReactNode} from 'react';
+import {Navigation} from '../components/shared/navigation/Navigation';
+
+export default function RootLayout({children}: {children: ReactNode}) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/app/page.tsx b/packages/classy-store/tests/next/fixture/app/page.tsx
new file mode 100644
index 0000000..69d957e
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/app/page.tsx
@@ -0,0 +1,22 @@
+import {FixtureStoreProvider} from '../components/shared/fixtureStoreProvider/FixtureStoreProvider';
+import {StoreFields} from '../components/shared/storeFields/StoreFields';
+import {createServerProof} from '../utils/serverProof/serverProof';
+
+type Props = {searchParams: Promise<{seed?: string}>};
+
+export const dynamic = 'force-dynamic';
+
+export default async function HomePage({searchParams}: Props) {
+ const {seed = 'home'} = await searchParams;
+ const data = await createServerProof(seed);
+ return (
+
+ Packed App Router consumer
+ {data.seed}
+ {data.proof}
+
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/app/stream/page.tsx b/packages/classy-store/tests/next/fixture/app/stream/page.tsx
new file mode 100644
index 0000000..0ef0a77
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/app/stream/page.tsx
@@ -0,0 +1,28 @@
+import {Suspense} from 'react';
+import {ServerStream} from '../../components/pages/stream/serverStream/ServerStream';
+import {FixtureStoreProvider} from '../../components/shared/fixtureStoreProvider/FixtureStoreProvider';
+import {StoreSelector} from '../../components/shared/storeSelector/StoreSelector';
+
+type Props = {searchParams: Promise<{seed?: string; ticket?: string}>};
+
+export const dynamic = 'force-dynamic';
+
+export default async function StreamPage({searchParams}: Props) {
+ const {seed = 'stream', ticket} = await searchParams;
+ return (
+
+
+ Streaming store hydration
+
+ Loading fields}
+ >
+
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/app/workspace/[route]/page.tsx b/packages/classy-store/tests/next/fixture/app/workspace/[route]/page.tsx
new file mode 100644
index 0000000..54e515a
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/app/workspace/[route]/page.tsx
@@ -0,0 +1,23 @@
+import {notFound} from 'next/navigation';
+import {FixtureStoreProvider} from '../../../components/shared/fixtureStoreProvider/FixtureStoreProvider';
+import {StoreFields} from '../../../components/shared/storeFields/StoreFields';
+
+type Props = {params: Promise<{route: string}>};
+
+export const dynamic = 'force-dynamic';
+
+export default async function WorkspacePage({params}: Props) {
+ const {route} = await params;
+ if (route !== 'alpha' && route !== 'beta') notFound();
+ return (
+
+ {route}
+ {crypto.randomUUID()}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/app/workspace/layout.tsx b/packages/classy-store/tests/next/fixture/app/workspace/layout.tsx
new file mode 100644
index 0000000..d441dd7
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/app/workspace/layout.tsx
@@ -0,0 +1,20 @@
+import type {ReactNode} from 'react';
+import {WorkspaceRefresh} from '../../components/pages/workspace/workspaceRefresh/WorkspaceRefresh';
+import {FixtureStoreProvider} from '../../components/shared/fixtureStoreProvider/FixtureStoreProvider';
+import {StoreFields} from '../../components/shared/storeFields/StoreFields';
+
+export default function WorkspaceLayout({children}: {children: ReactNode}) {
+ return (
+
+
+ Shared workspace
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/components/pages/stream/serverStream/ServerStream.tsx b/packages/classy-store/tests/next/fixture/components/pages/stream/serverStream/ServerStream.tsx
new file mode 100644
index 0000000..88270af
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/pages/stream/serverStream/ServerStream.tsx
@@ -0,0 +1,9 @@
+import {waitForServerGate} from '../../../../utils/serverGate/serverGate';
+import {StreamGate} from '../streamGate/StreamGate';
+
+type Props = {ticket?: string};
+
+export async function ServerStream({ticket}: Props) {
+ if (ticket) await waitForServerGate(ticket);
+ return ;
+}
diff --git a/packages/classy-store/tests/next/fixture/components/pages/stream/streamFields/StreamFields.tsx b/packages/classy-store/tests/next/fixture/components/pages/stream/streamFields/StreamFields.tsx
new file mode 100644
index 0000000..619a2ba
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/pages/stream/streamFields/StreamFields.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import {useEffect} from 'react';
+import {getDiagnostics} from '../../../../utils/diagnostics/diagnostics';
+import {StoreFields} from '../../../shared/storeFields/StoreFields';
+
+export function StreamFields() {
+ useEffect(() => {
+ getDiagnostics().streamChildHydrated = true;
+ }, []);
+ return ;
+}
diff --git a/packages/classy-store/tests/next/fixture/components/pages/stream/streamGate/StreamGate.tsx b/packages/classy-store/tests/next/fixture/components/pages/stream/streamGate/StreamGate.tsx
new file mode 100644
index 0000000..35696ce
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/pages/stream/streamGate/StreamGate.tsx
@@ -0,0 +1,10 @@
+'use client';
+
+import {waitForClientGate} from '../../../../utils/clientGate/clientGate';
+import {StreamFields} from '../streamFields/StreamFields';
+
+export function StreamGate() {
+ // Model a delayed client chunk: no consumer hook runs before gate release.
+ waitForClientGate();
+ return ;
+}
diff --git a/packages/classy-store/tests/next/fixture/components/pages/workspace/workspaceRefresh/WorkspaceRefresh.tsx b/packages/classy-store/tests/next/fixture/components/pages/workspace/workspaceRefresh/WorkspaceRefresh.tsx
new file mode 100644
index 0000000..98b5d5e
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/pages/workspace/workspaceRefresh/WorkspaceRefresh.tsx
@@ -0,0 +1,16 @@
+'use client';
+
+import {useRouter} from 'next/navigation';
+
+export function WorkspaceRefresh() {
+ const router = useRouter();
+ return (
+ router.refresh()}
+ >
+ Refresh server content
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/components/shared/fixtureStoreProvider/FixtureStoreProvider.tsx b/packages/classy-store/tests/next/fixture/components/shared/fixtureStoreProvider/FixtureStoreProvider.tsx
new file mode 100644
index 0000000..b0d1a85
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/shared/fixtureStoreProvider/FixtureStoreProvider.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import {
+ prepareStoreForHydration,
+ useLocalStore,
+} from '@codebelt/classy-store/react';
+import {persist} from '@codebelt/classy-store/utils';
+import {type ReactNode, useEffect, version} from 'react';
+import {FixtureStore} from '../../../stores/fixtureStore/fixtureStore.store';
+import {FixtureStoreContext} from '../../../stores/fixtureStore/fixtureStoreContext';
+import type {StoreData, StoreScope} from '../../../types/fixture.types';
+import {getDiagnostics} from '../../../utils/diagnostics/diagnostics';
+import {rememberDisposedStore} from '../../../utils/disposedStores/disposedStores';
+import {StoreSelector} from '../storeSelector/StoreSelector';
+import {StoreTracked} from '../storeTracked/StoreTracked';
+
+type Props = {
+ initialData: StoreData;
+ scope: StoreScope;
+ children: ReactNode;
+};
+
+export function FixtureStoreProvider({initialData, scope, children}: Props) {
+ const store = prepareStoreForHydration(
+ useLocalStore(() => new FixtureStore(initialData)),
+ );
+
+ useEffect(() => {
+ let active = true;
+ window.__classyNextCompilerSources = {
+ selector: Function.prototype.toString.call(StoreSelector),
+ tracked: Function.prototype.toString.call(StoreTracked),
+ };
+ window.__classyNextReactVersion = version;
+ const diagnostics = getDiagnostics();
+ diagnostics[`${scope}Mounts`]++;
+ diagnostics.activePersistence++;
+ if (scope === 'stream') {
+ diagnostics.streamParentHydrated = true;
+ diagnostics.streamRestored = false;
+ diagnostics.streamChildHydrated = false;
+ }
+ const handle = persist(store, {
+ name: `classy-next-${scope}`,
+ skipHydration: true,
+ storage: {
+ getItem: (name) => localStorage.getItem(name),
+ setItem: (name, value) => {
+ diagnostics.persistenceWrites++;
+ localStorage.setItem(name, value);
+ },
+ removeItem: (name) => localStorage.removeItem(name),
+ },
+ });
+ // Workspace stores start afresh when their scope ends. Only the streaming
+ // fixture opts into restoring saved data during hydration.
+ if (scope === 'stream') {
+ void handle.rehydrate().then(() => {
+ if (!active) return;
+ diagnostics.persistenceRestores++;
+ diagnostics.streamRestored = true;
+ diagnostics.streamRestoredValue = `${store.text}:${store.count}`;
+ });
+ }
+ return () => {
+ active = false;
+ handle.unsubscribe();
+ rememberDisposedStore(scope, store);
+ diagnostics[`${scope}Disposes`]++;
+ diagnostics.activePersistence--;
+ };
+ }, [scope, store]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/components/shared/navigation/Navigation.tsx b/packages/classy-store/tests/next/fixture/components/shared/navigation/Navigation.tsx
new file mode 100644
index 0000000..328fa3c
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/shared/navigation/Navigation.tsx
@@ -0,0 +1,20 @@
+import Link from 'next/link';
+
+export function Navigation() {
+ return (
+
+
+ Home
+ {' '}
+
+ Alpha
+ {' '}
+
+ Beta
+ {' '}
+
+ Stream
+
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/components/shared/storeFields/StoreFields.tsx b/packages/classy-store/tests/next/fixture/components/shared/storeFields/StoreFields.tsx
new file mode 100644
index 0000000..a0770e0
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/shared/storeFields/StoreFields.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useFixtureStore} from '../../../hooks/useFixtureStore/useFixtureStore';
+import {StoreSelector} from '../storeSelector/StoreSelector';
+import {StoreTracked} from '../storeTracked/StoreTracked';
+
+type Props = {prefix: string; includeCount?: boolean};
+
+export function StoreFields({prefix, includeCount = false}: Props) {
+ const store = useFixtureStore();
+ const state = useClassyStore(store);
+ return (
+
+ );
+}
diff --git a/packages/classy-store/tests/next/fixture/components/shared/storeSelector/StoreSelector.tsx b/packages/classy-store/tests/next/fixture/components/shared/storeSelector/StoreSelector.tsx
new file mode 100644
index 0000000..09f9eac
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/shared/storeSelector/StoreSelector.tsx
@@ -0,0 +1,14 @@
+'use client';
+
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useFixtureStore} from '../../../hooks/useFixtureStore/useFixtureStore';
+
+type Props = {testId: string; includeCount?: boolean};
+
+export function StoreSelector({testId, includeCount = false}: Props) {
+ const store = useFixtureStore();
+ const value = useClassyStore(store, (state) =>
+ includeCount ? `${state.text}:${state.count}` : state.text,
+ );
+ return {value} ;
+}
diff --git a/packages/classy-store/tests/next/fixture/components/shared/storeTracked/StoreTracked.tsx b/packages/classy-store/tests/next/fixture/components/shared/storeTracked/StoreTracked.tsx
new file mode 100644
index 0000000..43d2111
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/components/shared/storeTracked/StoreTracked.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useFixtureStore} from '../../../hooks/useFixtureStore/useFixtureStore';
+
+type Props = {testId: string; includeCount?: boolean};
+
+export function StoreTracked({testId, includeCount = false}: Props) {
+ const state = useClassyStore(useFixtureStore());
+ const value = includeCount ? `${state.text}:${state.count}` : state.text;
+ return {value} ;
+}
diff --git a/packages/classy-store/tests/next/fixture/hooks/useFixtureStore/useFixtureStore.ts b/packages/classy-store/tests/next/fixture/hooks/useFixtureStore/useFixtureStore.ts
new file mode 100644
index 0000000..504db5c
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/hooks/useFixtureStore/useFixtureStore.ts
@@ -0,0 +1,10 @@
+'use client';
+
+import {useContext} from 'react';
+import {FixtureStoreContext} from '../../stores/fixtureStore/fixtureStoreContext';
+
+export function useFixtureStore() {
+ const store = useContext(FixtureStoreContext);
+ if (!store) throw new Error('FixtureStoreProvider is required');
+ return store;
+}
diff --git a/packages/classy-store/tests/next/fixture/next.config.mjs b/packages/classy-store/tests/next/fixture/next.config.mjs
new file mode 100644
index 0000000..32d9fd9
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/next.config.mjs
@@ -0,0 +1,10 @@
+const compiler = process.env.NEXT_TEST_COMPILER === 'on';
+
+export default {
+ reactCompiler: compiler,
+ // The default App Router navigation contract. Activity-preserved routes are
+ // a separate opt-in feature and are not represented as unmount/reset tests.
+ cacheComponents: false,
+ distDir: compiler ? '.next-on' : '.next-off',
+ poweredByHeader: false,
+};
diff --git a/packages/classy-store/tests/next/fixture/stores/fixtureStore/fixtureStore.store.ts b/packages/classy-store/tests/next/fixture/stores/fixtureStore/fixtureStore.store.ts
new file mode 100644
index 0000000..1f31a44
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/stores/fixtureStore/fixtureStore.store.ts
@@ -0,0 +1,11 @@
+import type {StoreData} from '../../types/fixture.types';
+
+export class FixtureStore {
+ text: string;
+ count: number;
+
+ constructor(initialData: StoreData) {
+ this.text = initialData.text;
+ this.count = initialData.count;
+ }
+}
diff --git a/packages/classy-store/tests/next/fixture/stores/fixtureStore/fixtureStoreContext.ts b/packages/classy-store/tests/next/fixture/stores/fixtureStore/fixtureStoreContext.ts
new file mode 100644
index 0000000..65a71ed
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/stores/fixtureStore/fixtureStoreContext.ts
@@ -0,0 +1,6 @@
+'use client';
+
+import {createContext} from 'react';
+import type {FixtureStore} from './fixtureStore.store';
+
+export const FixtureStoreContext = createContext(null);
diff --git a/packages/classy-store/tests/next/fixture/tsconfig.json b/packages/classy-store/tests/next/fixture/tsconfig.json
new file mode 100644
index 0000000..e922a74
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [{"name": "next"}]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next-off/types/**/*.ts",
+ ".next-on/types/**/*.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/classy-store/tests/next/fixture/types/fixture.types.ts b/packages/classy-store/tests/next/fixture/types/fixture.types.ts
new file mode 100644
index 0000000..d0a1508
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/types/fixture.types.ts
@@ -0,0 +1,31 @@
+export type StoreData = {text: string; count: number};
+
+export type StoreScope = 'shared' | 'route' | 'stream' | 'request';
+
+export type FixtureDiagnostics = {
+ sharedMounts: number;
+ sharedDisposes: number;
+ routeMounts: number;
+ routeDisposes: number;
+ streamMounts: number;
+ streamDisposes: number;
+ requestMounts: number;
+ requestDisposes: number;
+ activePersistence: number;
+ persistenceRestores: number;
+ persistenceWrites: number;
+ streamParentHydrated: boolean;
+ streamRestored: boolean;
+ streamChildHydrated: boolean;
+ streamRestoredValue: string;
+};
+
+declare global {
+ interface Window {
+ __classyNextReactVersion?: string;
+ __classyNextDiagnostics?: FixtureDiagnostics;
+ __classyNextReleaseStream?: () => void;
+ __classyNextMutateDisposedStores?: () => number;
+ __classyNextCompilerSources?: {selector: string; tracked: string};
+ }
+}
diff --git a/packages/classy-store/tests/next/fixture/utils/clientGate/clientGate.ts b/packages/classy-store/tests/next/fixture/utils/clientGate/clientGate.ts
new file mode 100644
index 0000000..6fbf17d
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/utils/clientGate/clientGate.ts
@@ -0,0 +1,13 @@
+let ready = false;
+let pending: Promise | undefined;
+
+export function waitForClientGate() {
+ if (typeof window === 'undefined' || ready) return;
+ pending ??= new Promise((resolve) => {
+ window.__classyNextReleaseStream = () => {
+ ready = true;
+ resolve();
+ };
+ });
+ throw pending;
+}
diff --git a/packages/classy-store/tests/next/fixture/utils/diagnostics/diagnostics.ts b/packages/classy-store/tests/next/fixture/utils/diagnostics/diagnostics.ts
new file mode 100644
index 0000000..603f5e0
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/utils/diagnostics/diagnostics.ts
@@ -0,0 +1,23 @@
+import type {FixtureDiagnostics} from '../../types/fixture.types';
+
+// Diagnostics deliberately contain primitives, never stores or snapshots.
+export function getDiagnostics(): FixtureDiagnostics {
+ window.__classyNextDiagnostics ??= {
+ sharedMounts: 0,
+ sharedDisposes: 0,
+ routeMounts: 0,
+ routeDisposes: 0,
+ streamMounts: 0,
+ streamDisposes: 0,
+ requestMounts: 0,
+ requestDisposes: 0,
+ activePersistence: 0,
+ persistenceRestores: 0,
+ persistenceWrites: 0,
+ streamParentHydrated: false,
+ streamRestored: false,
+ streamChildHydrated: false,
+ streamRestoredValue: '',
+ };
+ return window.__classyNextDiagnostics;
+}
diff --git a/packages/classy-store/tests/next/fixture/utils/disposedStores/disposedStores.ts b/packages/classy-store/tests/next/fixture/utils/disposedStores/disposedStores.ts
new file mode 100644
index 0000000..9f61f53
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/utils/disposedStores/disposedStores.ts
@@ -0,0 +1,21 @@
+import type {FixtureStore} from '../../stores/fixtureStore/fixtureStore.store';
+import type {StoreScope} from '../../types/fixture.types';
+
+// Keep at most one deliberately retained store per workspace scope so browser
+// tests can mutate it after unmount and verify persistence actually unsubscribed.
+// The probe releases both references immediately after those mutations.
+const disposedStores = new Map();
+
+export function rememberDisposedStore(scope: StoreScope, store: FixtureStore) {
+ if (scope !== 'shared' && scope !== 'route') return;
+ disposedStores.set(scope, store);
+ window.__classyNextMutateDisposedStores = () => {
+ const count = disposedStores.size;
+ for (const disposed of disposedStores.values()) {
+ disposed.count++;
+ disposed.text = `disposed:${disposed.count}`;
+ }
+ disposedStores.clear();
+ return count;
+ };
+}
diff --git a/packages/classy-store/tests/next/fixture/utils/serverGate/serverGate.ts b/packages/classy-store/tests/next/fixture/utils/serverGate/serverGate.ts
new file mode 100644
index 0000000..d5fa4f6
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/utils/serverGate/serverGate.ts
@@ -0,0 +1,36 @@
+import type {ServerGate} from './serverGate.types';
+
+// Next can compile route handlers and RSC modules separately. This test-only
+// registry shares their gate signals within the production server process.
+const registry = globalThis as typeof globalThis & {
+ __classyNextServerGates?: Map;
+};
+
+function getGate(ticket: string): ServerGate {
+ registry.__classyNextServerGates ??= new Map();
+ const previous = registry.__classyNextServerGates.get(ticket);
+ if (previous) return previous;
+ let release = () => {};
+ const pending = new Promise((resolve) => {
+ release = resolve;
+ });
+ const timer = setTimeout(() => {
+ release();
+ registry.__classyNextServerGates?.delete(ticket);
+ }, 30_000);
+ timer.unref();
+ const gate = {pending, release, timer};
+ registry.__classyNextServerGates.set(ticket, gate);
+ return gate;
+}
+
+export async function waitForServerGate(ticket: string) {
+ const gate = getGate(ticket);
+ await gate.pending;
+ clearTimeout(gate.timer);
+ registry.__classyNextServerGates?.delete(ticket);
+}
+
+export function releaseServerGate(ticket: string) {
+ registry.__classyNextServerGates?.get(ticket)?.release();
+}
diff --git a/packages/classy-store/tests/next/fixture/utils/serverGate/serverGate.types.ts b/packages/classy-store/tests/next/fixture/utils/serverGate/serverGate.types.ts
new file mode 100644
index 0000000..64a7728
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/utils/serverGate/serverGate.types.ts
@@ -0,0 +1,5 @@
+export type ServerGate = {
+ pending: Promise;
+ release: () => void;
+ timer: ReturnType;
+};
diff --git a/packages/classy-store/tests/next/fixture/utils/serverProof/serverProof.ts b/packages/classy-store/tests/next/fixture/utils/serverProof/serverProof.ts
new file mode 100644
index 0000000..2ce075c
--- /dev/null
+++ b/packages/classy-store/tests/next/fixture/utils/serverProof/serverProof.ts
@@ -0,0 +1,34 @@
+import {createClassyStore, snapshot} from '@codebelt/classy-store';
+import {reactiveMap, reactiveSet} from '@codebelt/classy-store/collections';
+import {prepareStoreForHydration} from '@codebelt/classy-store/react/server';
+import {subscribeKey} from '@codebelt/classy-store/utils';
+
+export async function createServerProof(seed: string) {
+ // Every request owns its store. Only plain data crosses the RSC boundary.
+ const store = prepareStoreForHydration(
+ createClassyStore({
+ text: seed,
+ values: reactiveMap([['seed', seed]]),
+ tags: reactiveSet([seed]),
+ }),
+ );
+ let updates = 0;
+ const unsubscribe = subscribeKey(store, 'text', () => {
+ updates++;
+ });
+ store.text = `${seed}:updated`;
+ // Yield to other HTTP requests while this request's mutable store is alive.
+ // A microtask-only yield would let an accidental module singleton escape the
+ // concurrent-request regression because each handler could finish in order.
+ await new Promise((resolve) => setTimeout(resolve, 25));
+ unsubscribe();
+ const current = snapshot(store);
+ if (current.values.get('seed') !== seed || !current.tags.has(seed)) {
+ throw new Error('Server collections lost request data');
+ }
+ return {
+ seed,
+ proof: `${seed}|${current.text}|${updates}`,
+ initialData: {text: `server:${seed}`, count: 0},
+ };
+}
diff --git a/packages/classy-store/tests/next/next.spec.ts b/packages/classy-store/tests/next/next.spec.ts
new file mode 100644
index 0000000..39d8e95
--- /dev/null
+++ b/packages/classy-store/tests/next/next.spec.ts
@@ -0,0 +1,378 @@
+import {writeFile} from 'node:fs/promises';
+import {expect, type Page} from '@playwright/test';
+import {analyzeCompilerEvidence} from './compilerEvidence';
+import {test} from './next.test';
+import type {NextDiagnostics} from './next.types';
+
+async function diagnostics(page: Page): Promise {
+ return page.evaluate(() => Reflect.get(window, '__classyNextDiagnostics'));
+}
+
+async function expectTextConsumers(page: Page, scope: string, text: string) {
+ await expect(page.getByTestId(`${scope}-input`)).toHaveValue(text);
+ await expect(page.getByTestId(`${scope}-selector`)).toHaveText(text);
+ await expect(page.getByTestId(`${scope}-tracked`)).toHaveText(text);
+}
+
+test('the production consumers prove whether React Compiler transformed their hooks', async ({
+ page,
+}, testInfo) => {
+ await page.goto('/workspace/alpha');
+ await page.waitForFunction(() =>
+ Boolean(Reflect.get(window, '__classyNextCompilerSources')),
+ );
+ const sources: {selector: string; tracked: string} = await page.evaluate(() =>
+ Reflect.get(window, '__classyNextCompilerSources'),
+ );
+ expect(Object.keys(sources).sort()).toEqual(['selector', 'tracked']);
+ // Next App Router can use its vendored runtime rather than npm React's version.
+ const reactVersion = await page.evaluate(() =>
+ Reflect.get(window, '__classyNextReactVersion'),
+ );
+ expect(reactVersion).toEqual(expect.stringMatching(/^\d+\.\d+\.\d+/));
+ const evidence = Object.fromEntries(
+ Object.entries(sources).map(([consumer, source]) => [
+ consumer,
+ analyzeCompilerEvidence(source),
+ ]),
+ );
+ const sourcePath = testInfo.outputPath('production-consumer-functions.json');
+ await writeFile(
+ sourcePath,
+ JSON.stringify({reactVersion, sources, evidence}, null, 2),
+ );
+ await testInfo.attach('production-consumer-functions', {
+ path: sourcePath,
+ contentType: 'application/json',
+ });
+ for (const [consumer, source] of Object.entries(sources)) {
+ expect(source, `${consumer} has an inspectable function body`).toContain(
+ 'function',
+ );
+ const {slotCount, readSlots, writtenSlots} = evidence[consumer];
+ if (process.env.NEXT_TEST_COMPILER === 'on') {
+ expect(
+ slotCount,
+ `${consumer} allocates Compiler memo-cache slots`,
+ ).toBeGreaterThan(0);
+ expect(
+ readSlots.length,
+ `${consumer} reads cached slots`,
+ ).toBeGreaterThan(0);
+ expect(
+ writtenSlots.length,
+ `${consumer} writes cached slots`,
+ ).toBeGreaterThan(0);
+ expect(Math.max(...readSlots, ...writtenSlots)).toBeLessThan(slotCount);
+ } else {
+ expect(
+ slotCount,
+ `${consumer} has no Compiler memo-cache allocation`,
+ ).toBe(0);
+ }
+ }
+});
+
+test('packed server imports create isolated request stores', async ({
+ request,
+}) => {
+ const seeds = Array.from({length: 6}, () => crypto.randomUUID());
+ const responses = await Promise.all(
+ seeds.map(async (seed) => {
+ const response = await request.get(`/?seed=${seed}`);
+ expect(response.status()).toBe(200);
+ return {seed, html: await response.text()};
+ }),
+ );
+ for (const {seed, html} of responses) {
+ expect(html).toContain(`data-testid="request-seed">${seed}`);
+ expect(html).toContain(
+ `data-testid="server-imports">${seed}|${seed}:updated|1`,
+ );
+ for (const consumer of ['selector', 'tracked']) {
+ expect(html).toContain(
+ `data-testid="request-${consumer}">server:${seed}`,
+ );
+ }
+ for (const otherSeed of seeds) {
+ if (otherSeed !== seed) expect(html).not.toContain(otherSeed);
+ }
+ }
+});
+
+test('selectors and auto-tracking keep real controlled inputs and caret positions current', async ({
+ page,
+}) => {
+ await page.goto('/workspace/alpha');
+ await expectTextConsumers(page, 'workspace-shared', 'shared:initial');
+ await expectTextConsumers(page, 'route', 'alpha:initial');
+ const sharedInput = page.getByTestId('workspace-shared-input');
+ await sharedInput.fill('Shared');
+ await sharedInput.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 2),
+ );
+ await sharedInput.pressSequentially('XY');
+ await expectTextConsumers(page, 'workspace-shared', 'ShXYared');
+ expect(
+ await sharedInput.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(4);
+
+ const routeInput = page.getByTestId('route-input');
+ await routeInput.fill('Local');
+ await routeInput.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(1, 1),
+ );
+ await routeInput.pressSequentially('12');
+ await expectTextConsumers(page, 'route', 'L12ocal');
+ expect(
+ await routeInput.evaluate(
+ (element: HTMLInputElement) => element.selectionStart,
+ ),
+ ).toBe(3);
+ await expectTextConsumers(page, 'workspace-shared', 'ShXYared');
+});
+
+test('layout stores survive client navigation while route stores reset and clean up', async ({
+ page,
+}) => {
+ await page.goto('/workspace/alpha');
+ await expect(page.getByTestId('current-route')).toHaveText('alpha');
+ await expect.poll(async () => (await diagnostics(page)).routeMounts).toBe(1);
+ expect(await diagnostics(page)).toMatchObject({
+ sharedMounts: 1,
+ activePersistence: 2,
+ });
+ const documentSentinel = crypto.randomUUID();
+ await page.evaluate((sentinel) => {
+ Reflect.set(window, '__classyNextDocumentSentinel', sentinel);
+ }, documentSentinel);
+ await page.getByTestId('workspace-shared-input').fill('shared across routes');
+ await page.getByTestId('route-input').fill('alpha edited');
+
+ await page.getByTestId('link-beta').click();
+ await expect(page).toHaveURL(/\/workspace\/beta$/);
+ await expect(page.getByTestId('current-route')).toHaveText('beta');
+ await expectTextConsumers(page, 'workspace-shared', 'shared across routes');
+ await expectTextConsumers(page, 'route', 'beta:initial');
+ await expect
+ .poll(async () => (await diagnostics(page)).routeDisposes)
+ .toBe(1);
+ await page.getByTestId('route-input').fill('beta edited');
+
+ await page.goBack();
+ await expect(page).toHaveURL(/\/workspace\/alpha$/);
+ await expectTextConsumers(page, 'workspace-shared', 'shared across routes');
+ await expectTextConsumers(page, 'route', 'alpha:initial');
+ await expect
+ .poll(async () => (await diagnostics(page)).routeDisposes)
+ .toBe(2);
+
+ await page.goForward();
+ await expect(page).toHaveURL(/\/workspace\/beta$/);
+ await expectTextConsumers(page, 'workspace-shared', 'shared across routes');
+ await expectTextConsumers(page, 'route', 'beta:initial');
+ await expect
+ .poll(async () => (await diagnostics(page)).routeDisposes)
+ .toBe(3);
+ await page.getByTestId('route-input').fill('beta refreshed');
+ const beforeRefresh = await diagnostics(page);
+ const previousRequest = await page
+ .getByTestId('route-request-id')
+ .textContent();
+ const refreshed = page.waitForResponse(
+ (response) =>
+ response.url().includes('/workspace/beta') &&
+ response.request().headers().rsc === '1',
+ );
+ await page.getByTestId('workspace-refresh').click();
+ expect((await refreshed).ok()).toBe(true);
+ await expect(page.getByTestId('route-request-id')).not.toHaveText(
+ previousRequest ?? '',
+ );
+ await expectTextConsumers(page, 'workspace-shared', 'shared across routes');
+ await expectTextConsumers(page, 'route', 'beta refreshed');
+ expect(await diagnostics(page)).toMatchObject({
+ sharedMounts: 1,
+ sharedDisposes: 0,
+ routeMounts: beforeRefresh.routeMounts,
+ routeDisposes: beforeRefresh.routeDisposes,
+ activePersistence: 2,
+ });
+
+ await page.getByTestId('link-home').click();
+ await expect(page.getByTestId('server-imports')).toBeVisible();
+ await expect
+ .poll(async () => (await diagnostics(page)).sharedDisposes)
+ .toBe(1);
+ await expect
+ .poll(async () => (await diagnostics(page)).routeDisposes)
+ .toBe(4);
+ await expect
+ .poll(async () => (await diagnostics(page)).activePersistence)
+ .toBe(1);
+ const writesBeforeRetiredStoreMutations = (await diagnostics(page))
+ .persistenceWrites;
+ expect(
+ await page.evaluate(async () => {
+ const mutated = Reflect.get(window, '__classyNextMutateDisposedStores')();
+ await Promise.resolve();
+ await Promise.resolve();
+ return mutated;
+ }),
+ ).toBe(2);
+ expect((await diagnostics(page)).persistenceWrites).toBe(
+ writesBeforeRetiredStoreMutations,
+ );
+ expect(
+ await page.evaluate(() =>
+ Reflect.get(window, '__classyNextDocumentSentinel'),
+ ),
+ ).toBe(documentSentinel);
+
+ await page.getByTestId('link-alpha').click();
+ await expectTextConsumers(page, 'workspace-shared', 'shared:initial');
+ await expectTextConsumers(page, 'route', 'alpha:initial');
+ await expect.poll(async () => (await diagnostics(page)).sharedMounts).toBe(2);
+ await expect.poll(async () => (await diagnostics(page)).routeMounts).toBe(5);
+ expect(await diagnostics(page)).toMatchObject({activePersistence: 2});
+});
+
+test('the production response streams its fallback before the server boundary resolves', async ({
+ baseURL,
+ request,
+}) => {
+ const ticket = crypto.randomUUID();
+ const seed = crypto.randomUUID();
+ const response = await fetch(
+ new URL(`/stream?seed=${seed}&ticket=${ticket}`, baseURL),
+ {signal: AbortSignal.timeout(20_000)},
+ );
+ expect(response.status).toBe(200);
+ const reader = response.body?.getReader();
+ if (!reader) throw new Error('The streamed response has no readable body');
+ const decoder = new TextDecoder();
+ let html = '';
+ let released = false;
+ try {
+ while (!html.includes('data-testid="stream-fallback"')) {
+ const chunk = await reader.read();
+ expect(
+ chunk.done,
+ 'The fallback must arrive while the server gate is closed',
+ ).toBe(false);
+ html += decoder.decode(chunk.value, {stream: true});
+ }
+ expect(html).not.toContain('data-testid="stream-selector"');
+ expect(html).not.toContain('data-testid="stream-tracked"');
+ const release = await request.post('/api/stream-gate', {data: {ticket}});
+ expect(release.ok()).toBe(true);
+ released = true;
+ for (;;) {
+ const chunk = await reader.read();
+ if (chunk.done) break;
+ html += decoder.decode(chunk.value, {stream: true});
+ }
+ html += decoder.decode();
+ expect(html).toContain(
+ `data-testid="stream-selector">server:${seed}:0`,
+ );
+ expect(html).toContain(
+ `data-testid="stream-tracked">server:${seed}:0`,
+ );
+ expect(html.indexOf('data-testid="stream-selector"')).toBeGreaterThan(
+ html.indexOf('data-testid="stream-fallback"'),
+ );
+ } finally {
+ if (!released) await request.post('/api/stream-gate', {data: {ticket}});
+ await reader.cancel();
+ }
+});
+
+test('persistence restores before a streamed boundary hydrates without replacing server DOM', async ({
+ page,
+ request,
+}) => {
+ const ticket = crypto.randomUUID();
+ const seed = crypto.randomUUID();
+ await page.addInitScript((persistedText) => {
+ localStorage.setItem(
+ 'classy-next-stream',
+ JSON.stringify({version: 0, state: {text: persistedText, count: 7}}),
+ );
+ }, `persisted:${seed}`);
+ await page.goto(`/stream?seed=${seed}&ticket=${ticket}`, {
+ waitUntil: 'commit',
+ });
+ await expect(page.getByTestId('stream-fallback')).toBeVisible();
+ expect((await request.post('/api/stream-gate', {data: {ticket}})).ok()).toBe(
+ true,
+ );
+ await expect(page.getByTestId('stream-selector')).toHaveText(
+ `server:${seed}:0`,
+ );
+ await expect(page.getByTestId('stream-tracked')).toHaveText(
+ `server:${seed}:0`,
+ );
+ await expect(page.getByTestId('stream-input')).toHaveValue(`server:${seed}`);
+ await expect
+ .poll(async () => (await diagnostics(page)).streamRestored)
+ .toBe(true);
+ await expect(page.getByTestId('stream-restored-value')).toHaveText(
+ `persisted:${seed}:7`,
+ );
+ await page.waitForFunction(
+ () =>
+ typeof Reflect.get(window, '__classyNextReleaseStream') === 'function',
+ );
+ expect(await diagnostics(page)).toMatchObject({
+ streamParentHydrated: true,
+ streamChildHydrated: false,
+ streamRestoredValue: `persisted:${seed}:7`,
+ persistenceRestores: 1,
+ });
+ const serverElements = await page.evaluateHandle(() => [
+ document.querySelector('[data-testid="stream-selector"]'),
+ document.querySelector('[data-testid="stream-tracked"]'),
+ document.querySelector('[data-testid="stream-input"]'),
+ ]);
+ await page.evaluate(() => Reflect.get(window, '__classyNextReleaseStream')());
+ await expect(page.getByTestId('stream-selector')).toHaveText(
+ `persisted:${seed}:7`,
+ );
+ await expect(page.getByTestId('stream-tracked')).toHaveText(
+ `persisted:${seed}:7`,
+ );
+ await expect(page.getByTestId('stream-input')).toHaveValue(
+ `persisted:${seed}`,
+ );
+ await expect
+ .poll(async () => (await diagnostics(page)).streamChildHydrated)
+ .toBe(true);
+ expect(
+ await serverElements.evaluate((elements) => {
+ const current = [
+ document.querySelector('[data-testid="stream-selector"]'),
+ document.querySelector('[data-testid="stream-tracked"]'),
+ document.querySelector('[data-testid="stream-input"]'),
+ ];
+ return elements.every(
+ (element, index) => element !== null && element === current[index],
+ );
+ }),
+ ).toBe(true);
+ await serverElements.dispose();
+ const input = page.getByTestId('stream-input');
+ await input.fill('Edited');
+ await input.evaluate((element: HTMLInputElement) =>
+ element.setSelectionRange(2, 2),
+ );
+ await input.pressSequentially('XY');
+ await expect(input).toHaveValue('EdXYited');
+ await expect(page.getByTestId('stream-selector')).toHaveText('EdXYited:10');
+ await expect(page.getByTestId('stream-tracked')).toHaveText('EdXYited:10');
+ expect(
+ await input.evaluate((element: HTMLInputElement) => element.selectionStart),
+ ).toBe(4);
+});
diff --git a/packages/classy-store/tests/next/next.test.ts b/packages/classy-store/tests/next/next.test.ts
new file mode 100644
index 0000000..521c13e
--- /dev/null
+++ b/packages/classy-store/tests/next/next.test.ts
@@ -0,0 +1,19 @@
+import {test as base, expect} from '@playwright/test';
+
+export const test = base.extend<{browserErrors: undefined}>({
+ browserErrors: [
+ async ({page}, use) => {
+ const errors: string[] = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ page.on('console', (message) => {
+ if (message.type() === 'error') errors.push(message.text());
+ });
+ await use(undefined);
+ expect(
+ errors,
+ 'Browser exceptions, hydration errors, and console errors',
+ ).toEqual([]);
+ },
+ {auto: true},
+ ],
+});
diff --git a/packages/classy-store/tests/next/next.types.ts b/packages/classy-store/tests/next/next.types.ts
new file mode 100644
index 0000000..9aa0026
--- /dev/null
+++ b/packages/classy-store/tests/next/next.types.ts
@@ -0,0 +1,20 @@
+export type NextDiagnostics = {
+ sharedMounts: number;
+ sharedDisposes: number;
+ routeMounts: number;
+ routeDisposes: number;
+ persistenceRestores: number;
+ persistenceWrites: number;
+ activePersistence: number;
+ streamParentHydrated: boolean;
+ streamRestored: boolean;
+ streamChildHydrated: boolean;
+ streamRestoredValue: string;
+};
+
+export type CompilerEvidence = {
+ cacheVariable: string | null;
+ slotCount: number;
+ readSlots: number[];
+ writtenSlots: number[];
+};
diff --git a/packages/classy-store/tests/next/playwright.config.ts b/packages/classy-store/tests/next/playwright.config.ts
new file mode 100644
index 0000000..339b836
--- /dev/null
+++ b/packages/classy-store/tests/next/playwright.config.ts
@@ -0,0 +1,30 @@
+import {fileURLToPath} from 'node:url';
+import {defineConfig, devices} from '@playwright/test';
+
+const baseURL = process.env.NEXT_TEST_URL;
+if (!baseURL)
+ throw new Error('NEXT_TEST_URL must identify the production Next server');
+const compiler = process.env.NEXT_TEST_COMPILER;
+if (compiler !== 'on' && compiler !== 'off') {
+ throw new Error('NEXT_TEST_COMPILER must be on or off');
+}
+
+export default defineConfig({
+ testDir: '.',
+ testMatch: 'next.spec.ts',
+ fullyParallel: false,
+ workers: 1,
+ forbidOnly: Boolean(process.env.CI),
+ retries: 0,
+ timeout: 45_000,
+ reporter: 'list',
+ metadata: {compiler},
+ outputDir:
+ process.env.NEXT_TEST_OUTPUT ??
+ fileURLToPath(new URL('../../../../test-results/next', import.meta.url)),
+ use: {
+ baseURL,
+ trace: 'retain-on-failure',
+ ...devices['Desktop Chrome'],
+ },
+});
diff --git a/packages/classy-store/tests/next/runNext.mjs b/packages/classy-store/tests/next/runNext.mjs
new file mode 100644
index 0000000..ff0dff8
--- /dev/null
+++ b/packages/classy-store/tests/next/runNext.mjs
@@ -0,0 +1,297 @@
+import assert from 'node:assert/strict';
+import {spawn} from 'node:child_process';
+import {createWriteStream} from 'node:fs';
+import {
+ cp,
+ mkdir,
+ mkdtemp,
+ readFile,
+ rename,
+ rm,
+ writeFile,
+} from 'node:fs/promises';
+import {createRequire} from 'node:module';
+import {createServer} from 'node:net';
+import {tmpdir} from 'node:os';
+import {dirname, join, resolve} from 'node:path';
+import {fileURLToPath} from 'node:url';
+
+const directory = dirname(fileURLToPath(import.meta.url));
+const packageDirectory = resolve(directory, '../..');
+const require = createRequire(import.meta.url);
+const manifest = JSON.parse(
+ await readFile(join(packageDirectory, 'package.json'), 'utf8'),
+);
+const consumer = process.env.NEXT_TEST_DIRECTORY
+ ? resolve(process.env.NEXT_TEST_DIRECTORY)
+ : await mkdtemp(join(tmpdir(), 'classy-store-next-'));
+const output = resolve(
+ process.env.NEXT_TEST_OUTPUT ??
+ join(packageDirectory, '../../test-results/next'),
+);
+const modes = (process.env.NEXT_TEST_MODES ?? 'off,on').split(',');
+assert(modes.length && modes.every((mode) => mode === 'off' || mode === 'on'));
+const environment = {
+ ...process.env,
+ NODE_ENV: 'production',
+ NEXT_TELEMETRY_DISABLED: '1',
+};
+await mkdir(consumer, {recursive: true});
+await mkdir(output, {recursive: true});
+// Failed reruns must not leave an earlier successful report beside new logs.
+await rm(join(output, 'report.json'), {force: true});
+
+async function run(
+ label,
+ command,
+ argumentsList,
+ extraEnvironment = {},
+ cwd = consumer,
+) {
+ const log = createWriteStream(join(output, `${label}.log`));
+ const child = spawn(command, argumentsList, {
+ cwd,
+ env: {...environment, ...extraEnvironment},
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ let stdout = '';
+ child.stdout.on('data', (chunk) => {
+ stdout += chunk;
+ log.write(chunk);
+ });
+ child.stderr.on('data', (chunk) => log.write(chunk));
+ let forceStop;
+ const timeout = setTimeout(() => {
+ child.kill('SIGTERM');
+ forceStop = setTimeout(() => child.kill('SIGKILL'), 5000);
+ }, 300_000);
+ try {
+ await new Promise((resolveRun, reject) => {
+ child.once('error', reject);
+ child.once('close', (code, signal) =>
+ code === 0
+ ? resolveRun()
+ : reject(
+ new Error(
+ `${label} failed (${code ?? signal}); see ${join(output, `${label}.log`)}`,
+ ),
+ ),
+ );
+ });
+ } finally {
+ clearTimeout(timeout);
+ clearTimeout(forceStop);
+ log.end();
+ }
+ return stdout;
+}
+
+async function availablePort() {
+ const server = createServer();
+ await new Promise((resolvePort, reject) => {
+ server.once('error', reject);
+ server.listen(0, '127.0.0.1', resolvePort);
+ });
+ const port = server.address().port;
+ await new Promise((resolveClose, reject) =>
+ server.close((error) => (error ? reject(error) : resolveClose())),
+ );
+ return port;
+}
+
+async function stop(child) {
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null)
+ return;
+ child.kill('SIGTERM');
+ const timeout = setTimeout(() => child.kill('SIGKILL'), 5000);
+ try {
+ await new Promise((resolveExit) => child.once('exit', resolveExit));
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+const dependencies = {
+ next: '16.4.2',
+ react: manifest.devDependencies.react,
+ 'react-dom': manifest.devDependencies['react-dom'],
+ 'babel-plugin-react-compiler':
+ manifest.devDependencies['babel-plugin-react-compiler'],
+};
+const results = [];
+let succeeded = false;
+try {
+ console.log(`Packed Next.js consumer: ${consumer}`);
+ const packed = JSON.parse(
+ await run(
+ 'pack',
+ 'npm',
+ ['pack', '--ignore-scripts', '--json', '--pack-destination', consumer],
+ {},
+ packageDirectory,
+ ),
+ );
+ // Content-address the tarball so a reused consumer cannot silently reuse
+ // npm's installed copy of a previous build with the same package version.
+ const archive = join(consumer, `classy-${packed[0].shasum}.tgz`);
+ await rename(join(consumer, packed[0].filename), archive);
+ await cp(join(directory, 'fixture'), consumer, {recursive: true});
+ await writeFile(
+ join(consumer, 'package.json'),
+ JSON.stringify(
+ {
+ name: 'classy-store-next-consumer',
+ private: true,
+ type: 'module',
+ dependencies: {
+ ...dependencies,
+ '@codebelt/classy-store': `file:${archive}`,
+ },
+ devDependencies: {
+ typescript: manifest.devDependencies.typescript,
+ '@types/react': manifest.devDependencies['@types/react'],
+ '@types/react-dom': manifest.devDependencies['@types/react-dom'],
+ '@types/node': '26.6.5',
+ },
+ },
+ null,
+ 2,
+ ),
+ );
+ // Install from the tarball, never an alias or symlink to this checkout.
+ console.log('Installing the isolated package consumer');
+ await run('install', 'npm', [
+ 'install',
+ '--include=dev',
+ '--ignore-scripts',
+ '--no-audit',
+ '--no-fund',
+ ]);
+ const lock = JSON.parse(
+ await readFile(join(consumer, 'package-lock.json'), 'utf8'),
+ );
+ assert.equal(
+ lock.packages['node_modules/@codebelt/classy-store'].integrity,
+ packed[0].integrity,
+ );
+ const boundaryReport = JSON.parse(
+ await run('boundaries', process.execPath, [
+ join(directory, 'checkBoundaries.mjs'),
+ consumer,
+ ]),
+ );
+ await writeFile(
+ join(output, 'boundaries.json'),
+ JSON.stringify(boundaryReport, null, 2),
+ );
+ const next = join(consumer, 'node_modules/next/dist/bin/next');
+ const playwright = require.resolve('@playwright/test/cli');
+ for (const mode of modes) {
+ const modeEnvironment = {NEXT_TEST_COMPILER: mode};
+ // A reused consumer must not inherit cached type diagnostics or bundled
+ // package code from an earlier tarball with the same published version.
+ await rm(join(consumer, `.next-${mode}`), {recursive: true, force: true});
+ console.log(`Compiler ${mode}: production Next.js build (Turbopack)`);
+ await run(
+ `build-${mode}`,
+ process.execPath,
+ [next, 'build'],
+ modeEnvironment,
+ );
+ const port = await availablePort();
+ const url = `http://127.0.0.1:${port}`;
+ const serverLog = createWriteStream(join(output, `server-${mode}.log`));
+ const server = spawn(
+ process.execPath,
+ [next, 'start', '--hostname', '127.0.0.1', '--port', String(port)],
+ {
+ cwd: consumer,
+ env: {...environment, ...modeEnvironment},
+ stdio: ['ignore', 'pipe', 'pipe'],
+ },
+ );
+ server.stdout.pipe(serverLog, {end: false});
+ server.stderr.pipe(serverLog, {end: false});
+ let serverError;
+ server.on('error', (error) => {
+ serverError = error;
+ });
+ try {
+ let ready = false;
+ let readinessDetail = 'No response received';
+ for (let attempt = 0; attempt < 120; attempt += 1) {
+ if (serverError) throw serverError;
+ if (server.exitCode !== null)
+ throw new Error(`Next server exited; see server-${mode}.log`);
+ try {
+ const response = await fetch(url, {
+ signal: AbortSignal.timeout(1000),
+ });
+ ready = response.ok;
+ readinessDetail = `HTTP ${response.status}`;
+ await response.body?.cancel();
+ } catch (error) {
+ readinessDetail = String(error);
+ }
+ if (ready) break;
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
+ }
+ assert(
+ ready,
+ `Next server did not become ready: ${url}: ${readinessDetail}`,
+ );
+ console.log(`Compiler ${mode}: browser integration at ${url}`);
+ const testOutput = await run(
+ `browser-${mode}`,
+ process.execPath,
+ [
+ playwright,
+ 'test',
+ '--config',
+ join(directory, 'playwright.config.ts'),
+ ],
+ {
+ ...modeEnvironment,
+ NEXT_TEST_URL: url,
+ NEXT_CONSUMER_DIRECTORY: consumer,
+ NEXT_TEST_OUTPUT: join(output, mode),
+ },
+ packageDirectory,
+ );
+ console.log(testOutput.trim());
+ results.push({compiler: mode, passed: true});
+ } finally {
+ await stop(server);
+ serverLog.end();
+ }
+ }
+ await writeFile(
+ join(output, 'report.json'),
+ JSON.stringify(
+ {
+ createdAt: new Date().toISOString(),
+ dependencies,
+ library: {version: packed[0].version, integrity: packed[0].integrity},
+ bundler: 'Turbopack',
+ cacheComponents: false,
+ consumer,
+ boundaries: boundaryReport,
+ results,
+ },
+ null,
+ 2,
+ ),
+ );
+ succeeded = true;
+ console.log(`Next.js integration passed; report and logs: ${output}`);
+} finally {
+ if (
+ succeeded &&
+ !process.env.NEXT_TEST_DIRECTORY &&
+ process.env.KEEP_NEXT_TEMP !== '1'
+ ) {
+ await rm(consumer, {recursive: true, force: true});
+ } else {
+ console.log(`Kept Next.js consumer at ${consumer}`);
+ }
+}
diff --git a/packages/classy-store/tests/next/tsconfig.json b/packages/classy-store/tests/next/tsconfig.json
new file mode 100644
index 0000000..afa6c8f
--- /dev/null
+++ b/packages/classy-store/tests/next/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "../.."
+ },
+ "include": ["*.ts"],
+ "exclude": ["fixture"]
+}
diff --git a/packages/classy-store/tests/performance/README.md b/packages/classy-store/tests/performance/README.md
new file mode 100644
index 0000000..c554aac
--- /dev/null
+++ b/packages/classy-store/tests/performance/README.md
@@ -0,0 +1,84 @@
+# React performance at scale
+
+From the repository root, run `bun run benchmark:react`. Install Chromium first
+with `cd packages/classy-store && bunx playwright install chromium` if needed.
+The benchmark uses the built public library entrypoints, the installed React
+version, and a local browser server. It writes JSON to
+`test-results/react-performance.json` (ignored by Git).
+
+See [the initial before/after results](./RESULTS.md) for the snapshot-target cache
+optimization and its remaining costs.
+
+The matrix mounts 200 and 1,000 controlled inputs or list rows. Each workload
+runs with selectors and auto-tracking, with React Compiler off and on. Both
+variants use React's **production profiling build**; the library is uncompiled.
+The shared compiler helper rejects skipped components and requires memo slots
+and the compiler runtime import. Compilation records are saved with the results.
+
+For each case the runner opens five fresh pages, records mount time, warms up
+each operation five times, and records twenty iterations per operation:
+
+- Change one consumer's value.
+- Write ten different values in one synchronous batch.
+- Change an unread property or assign an unchanged value.
+- Change a parent prop used by every consumer.
+- For forms, type ten actual browser keystrokes into the middle of an input.
+
+Each operation checks exact consumer/root commit counts. No-op and unrelated
+writes must commit zero consumers; a single changed value commits one; a burst
+commits ten consumers in one root commit. The input check verifies final text,
+caret position, and one consumer commit per keystroke. After unmount, mutations
+must trigger neither selectors nor consumer effects. Console/page errors fail.
+
+## Measurements and limits
+
+JSON includes raw samples, medians/p95s, consumer commits, root commits,
+selector calls, Profiler rendering durations, dependency versions, browser/OS,
+and a configurable build label. Effects count consumer commits because Compiler
+can memoize expressions inside a component body. Selector calls are counted
+separately: a component avoiding a render does not mean its selector did no work.
+
+Operation duration covers synchronous notification, snapshot work, rendering,
+and commit inside `flushSync`. It excludes browser layout/paint after the task.
+Input latency runs from capture of the browser `input` event to the consumer's
+layout effect, **not** from hardware input to painted pixels. Mount includes
+store construction and React root creation. Profiling and counters add overhead
+to both variants. These are instrumented microbenchmarks, not application SLAs.
+
+Timing results are observations, **never CI pass/fail thresholds**. Run timing
+comparisons sequentially on an otherwise idle machine with the same browser,
+runtime, sizes, and sample settings. Cases use a fixed order; thermal state,
+JIT, garbage collection, timer resolution, and scheduling introduce noise.
+Use repeated runs and raw samples before interpreting small differences. The
+suite does not measure retained heap or establish absence of memory leaks.
+
+CI runs the full case matrix with one page and three iterations per operation.
+Only compilation, behavior, and exact commit budgets gate that smoke run;
+machine-dependent timing values do not.
+
+The normal compiler matrix has additional deterministic scale regressions at
+256 fields and 512 rows on React 18.0, 18.3, and 19, before and after compilation.
+It covers local/parent rerenders, memoized reads, multiple writes, and removal/
+remount. Source regressions also assert that hundreds of readers scan a shared
+frozen list only once and keep their dependencies and readonly targets isolated.
+
+## Reproducing a comparison
+
+Preserve the baseline `dist` directory before rebuilding the changed library.
+Both runs resolve React and proxy-compare from this checkout so only the library
+build changes. Run the script directly when supplying an existing build:
+
+```sh
+BENCHMARK_DIST=/absolute/path/to/baseline/dist \
+BENCHMARK_LABEL=baseline-commit \
+BENCHMARK_OUTPUT=/tmp/react-baseline.json \
+bun packages/classy-store/tests/performance/runPerformance.ts
+
+BENCHMARK_LABEL=optimized \
+BENCHMARK_OUTPUT=/tmp/react-optimized.json \
+bun run benchmark:react
+```
+
+Optional settings: `BENCHMARK_SIZES=200,1000`, `BENCHMARK_SAMPLES=5`, and
+`BENCHMARK_ITERATIONS=20`. Sizes must be at least ten for the burst workload.
+No new runtime dependency or public React API is required.
diff --git a/packages/classy-store/tests/performance/RESULTS.md b/packages/classy-store/tests/performance/RESULTS.md
new file mode 100644
index 0000000..cc39cc8
--- /dev/null
+++ b/packages/classy-store/tests/performance/RESULTS.md
@@ -0,0 +1,71 @@
+# React scale results β 2026-10-20
+
+Baseline: commit `5912dd4`. Optimized: guarded shared snapshot-target cache.
+Both builds used the same benchmark consumer and dependencies. Measurements
+were collected sequentially after other test processes finished.
+
+Environment: React 19.3.1, React Compiler 1.0.0, Chromium 156.0.8078.4,
+Bun 1.3.14, macOS Darwin 25.6.0 arm64. Both variants use production profiling.
+Each case used five fresh pages, five warmup operations, and twenty measured
+iterations per operation per page. See [methodology](./README.md) for limits.
+
+## 1,000 consumers
+
+Values are median milliseconds, baseline β optimized. Parent updates change
+a prop used by every consumer; single-value updates change only one consumer.
+
+| Workload | Subscription | Compiler | Mount | Parent update | Single update | Ten writes |
+|---|---|---|---:|---:|---:|---:|
+| form | selector | off | 24.9 β 25.1 | 3.6 β 3.5 | 0.5 β 0.5 | 1.4 β 1.4 |
+| form | selector | on | 25.6 β 26.0 | 3.8 β 3.7 | 0.5 β 0.5 | 1.4 β 1.4 |
+| form | tracked | off | 114.9 β 29.3 | 87.1 β 5.2 | 0.7 β 0.7 | 3.3 β 2.1 |
+| form | tracked | on | 115.2 β 31.3 | 87.3 β 5.7 | 0.7 β 0.7 | 3.2 β 2.2 |
+| list | selector | off | 17.1 β 16.9 | 3.8 β 4.0 | 0.5 β 0.5 | 1.4 β 1.4 |
+| list | selector | on | 17.1 β 17.3 | 3.9 β 4.0 | 0.5 β 0.5 | 1.4 β 1.4 |
+| list | tracked | off | 105.5 β 21.5 | 86.7 β 5.6 | 0.7 β 0.7 | 3.1 β 2.1 |
+| list | tracked | on | 107.4 β 22.9 | 86.7 β 6.3 | 0.7 β 0.7 | 3.1 β 2.2 |
+
+## 200 consumers
+
+| Workload | Subscription | Compiler | Mount | Parent update |
+|---|---|---|---:|---:|
+| form | selector | off | 9.0 β 8.7 | 0.8 β 0.7 |
+| form | selector | on | 9.8 β 9.6 | 0.8 β 0.7 |
+| form | tracked | off | 14.8 β 10.4 | 4.9 β 1.0 |
+| form | tracked | on | 15.0 β 11.0 | 4.9 β 1.0 |
+| list | selector | off | 8.1 β 7.9 | 0.8 β 0.8 |
+| list | selector | on | 8.7 β 8.9 | 0.8 β 0.8 |
+| list | tracked | off | 13.6 β 10.3 | 4.8 β 1.1 |
+| list | tracked | on | 13.6 β 10.3 | 4.9 β 1.2 |
+
+## Interpretation
+
+The removed bottleneck was repeated descriptor scanning and copying of frozen
+snapshot arrays for every auto-tracked consumer. Sharing guarded copies reduced
+the large auto-tracked mount and parent-update costs. Selector paths are unchanged.
+The deterministic source regression verifies one array descriptor scan across
+1,000 independent readers instead of relying on these machine-dependent timings.
+
+Single-consumer updates did not materially improve: all subscribers still check
+the new snapshot, even when only one consumer commits. At 1,000 fields, median
+input-event-to-layout-effect time was 0.5 β 0.5 ms for selectors and 0.7 β 0.8 ms
+for auto-tracking, with Compiler either off or on. These instrumented measurements
+do not establish an input-latency improvement or measure paint. Small differences
+are sensitive to timer resolution and scheduling.
+
+Compiler does not remove the store notification work. These specific workloads
+force relevant parent props to change, so they do not demonstrate every benefit
+of Compiler memoization. Existing compiler regressions separately cover skipped
+reads and memoized children.
+
+Every measured case passed exact commit budgets, no-op/unread update filtering,
+unmount checks, and (for forms) real typing/caret assertions. Five compiled
+components each reported successful compilation with memo slots. The cache keeps
+per-consumer proxies and reads separate, rejects reflective mutations of shared
+copies, and leaves mutable computed results uncached. Normal subscriptions retain
+their existing microtask batching; React notifications remain synchronous.
+
+Timing data is observational and is not a CI threshold. CI runs a reduced-sample
+version of the same matrix to check compilation, behavior, and commit counts.
+The runner writes raw timings, metrics, and compilation records to JSON for
+future comparisons. This run does not measure heap retention or prove leak freedom.
diff --git a/packages/classy-store/tests/performance/client.tsx b/packages/classy-store/tests/performance/client.tsx
new file mode 100644
index 0000000..0ea46e4
--- /dev/null
+++ b/packages/classy-store/tests/performance/client.tsx
@@ -0,0 +1,89 @@
+import {createClassyStore} from '@codebelt/classy-store';
+import {flushSync} from 'react-dom';
+import {createRoot, type Root} from 'react-dom/client';
+import {ScaleApp} from './components/scaleApp/ScaleApp';
+import {ScaleStore} from './performance.store';
+import type {ScaleConfiguration, ScaleOperation} from './performance.types';
+import {
+ readMetrics,
+ recordInputStart,
+ resetMetrics,
+} from './performanceMetrics';
+
+let root: Root | undefined;
+let store: ScaleStore;
+let configuration: ScaleConfiguration;
+let revision = 0;
+let sequence = 0;
+
+function render(): void {
+ root?.render(
+ ,
+ );
+}
+
+function mutate(operation: ScaleOperation): void {
+ const index = sequence % configuration.size;
+ sequence += 1;
+ switch (operation) {
+ case 'change':
+ store.items[index].text = `Changed ${sequence}`;
+ break;
+ case 'burst':
+ for (let offset = 0; offset < 10; offset += 1) {
+ store.items[(index + offset) % configuration.size].text =
+ `Burst ${sequence}-${offset}`;
+ }
+ break;
+ case 'unrelated':
+ store.unrelated += 1;
+ break;
+ case 'noop':
+ store.setText(index, store.items[index].text);
+ break;
+ case 'parent':
+ revision += 1;
+ render();
+ break;
+ }
+}
+
+document.addEventListener('input', recordInputStart, true);
+
+window.scaleBenchmark = {
+ mount(nextConfiguration) {
+ root?.unmount();
+ configuration = nextConfiguration;
+ revision = 0;
+ sequence = 0;
+ resetMetrics();
+ const started = performance.now();
+ store = createClassyStore(new ScaleStore(configuration.size));
+ root = createRoot(document.getElementById('root') as HTMLElement);
+ flushSync(render);
+ return {durationsMs: [performance.now() - started], metrics: readMetrics()};
+ },
+ measure(operation, iterations) {
+ resetMetrics();
+ const durationsMs: number[] = [];
+ for (let iteration = 0; iteration < iterations; iteration += 1) {
+ const started = performance.now();
+ flushSync(() => mutate(operation));
+ durationsMs.push(performance.now() - started);
+ }
+ return {durationsMs, metrics: readMetrics()};
+ },
+ resetMetrics,
+ readMetrics,
+ unmount() {
+ root?.unmount();
+ root = undefined;
+ resetMetrics();
+ store.items[0].text = 'After unmount';
+ return readMetrics();
+ },
+};
diff --git a/packages/classy-store/tests/performance/components/scaleApp/ScaleApp.tsx b/packages/classy-store/tests/performance/components/scaleApp/ScaleApp.tsx
new file mode 100644
index 0000000..961a6fc
--- /dev/null
+++ b/packages/classy-store/tests/performance/components/scaleApp/ScaleApp.tsx
@@ -0,0 +1,41 @@
+import {Profiler} from 'react';
+import type {ScaleStore} from '../../performance.store';
+import type {ScaleConfiguration} from '../../performance.types';
+import {recordRootCommit} from '../../performanceMetrics';
+import {SelectorField} from '../selectorField/SelectorField';
+import {SelectorRow} from '../selectorRow/SelectorRow';
+import {TrackedField} from '../trackedField/TrackedField';
+import {TrackedRow} from '../trackedRow/TrackedRow';
+
+type Props = {
+ configuration: ScaleConfiguration;
+ store: ScaleStore;
+ revision: number;
+};
+
+export function ScaleApp({configuration, store, revision}: Props) {
+ const Consumer =
+ configuration.workload === 'form'
+ ? configuration.mode === 'selector'
+ ? SelectorField
+ : TrackedField
+ : configuration.mode === 'selector'
+ ? SelectorRow
+ : TrackedRow;
+ const identities = Array.from({length: configuration.size}, (_, index) => ({
+ id: `item-${index}`,
+ index,
+ }));
+ const children = identities.map(({id, index}) => (
+
+ ));
+ return (
+
+ {configuration.workload === 'form' ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/packages/classy-store/tests/performance/components/selectorField/SelectorField.tsx b/packages/classy-store/tests/performance/components/selectorField/SelectorField.tsx
new file mode 100644
index 0000000..56a6bee
--- /dev/null
+++ b/packages/classy-store/tests/performance/components/selectorField/SelectorField.tsx
@@ -0,0 +1,22 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect} from 'react';
+import type {ScaleConsumerProps} from '../../performance.types';
+import {recordConsumerCommit, recordSelector} from '../../performanceMetrics';
+
+export function SelectorField({store, index, revision}: ScaleConsumerProps) {
+ const text = useClassyStore(store, (state) => {
+ recordSelector();
+ return state.items[index].text;
+ });
+ useLayoutEffect(recordConsumerCommit);
+ return (
+ {
+ store.setText(index, event.currentTarget.value);
+ }}
+ />
+ );
+}
diff --git a/packages/classy-store/tests/performance/components/selectorRow/SelectorRow.tsx b/packages/classy-store/tests/performance/components/selectorRow/SelectorRow.tsx
new file mode 100644
index 0000000..a639dfe
--- /dev/null
+++ b/packages/classy-store/tests/performance/components/selectorRow/SelectorRow.tsx
@@ -0,0 +1,17 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect} from 'react';
+import type {ScaleConsumerProps} from '../../performance.types';
+import {recordConsumerCommit, recordSelector} from '../../performanceMetrics';
+
+export function SelectorRow({store, index, revision}: ScaleConsumerProps) {
+ const item = useClassyStore(store, (state) => {
+ recordSelector();
+ return state.items[index];
+ });
+ useLayoutEffect(recordConsumerCommit);
+ return (
+
+ {item.text} β {item.completed ? 'done' : 'pending'}
+
+ );
+}
diff --git a/packages/classy-store/tests/performance/components/trackedField/TrackedField.tsx b/packages/classy-store/tests/performance/components/trackedField/TrackedField.tsx
new file mode 100644
index 0000000..e8354e4
--- /dev/null
+++ b/packages/classy-store/tests/performance/components/trackedField/TrackedField.tsx
@@ -0,0 +1,19 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect} from 'react';
+import type {ScaleConsumerProps} from '../../performance.types';
+import {recordConsumerCommit} from '../../performanceMetrics';
+
+export function TrackedField({store, index, revision}: ScaleConsumerProps) {
+ const state = useClassyStore(store);
+ useLayoutEffect(recordConsumerCommit);
+ return (
+ {
+ store.setText(index, event.currentTarget.value);
+ }}
+ />
+ );
+}
diff --git a/packages/classy-store/tests/performance/components/trackedRow/TrackedRow.tsx b/packages/classy-store/tests/performance/components/trackedRow/TrackedRow.tsx
new file mode 100644
index 0000000..3c58dab
--- /dev/null
+++ b/packages/classy-store/tests/performance/components/trackedRow/TrackedRow.tsx
@@ -0,0 +1,15 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useLayoutEffect} from 'react';
+import type {ScaleConsumerProps} from '../../performance.types';
+import {recordConsumerCommit} from '../../performanceMetrics';
+
+export function TrackedRow({store, index, revision}: ScaleConsumerProps) {
+ const state = useClassyStore(store);
+ const item = state.items[index];
+ useLayoutEffect(recordConsumerCommit);
+ return (
+
+ {item.text} β {item.completed ? 'done' : 'pending'}
+
+ );
+}
diff --git a/packages/classy-store/tests/performance/performance.store.ts b/packages/classy-store/tests/performance/performance.store.ts
new file mode 100644
index 0000000..085d17d
--- /dev/null
+++ b/packages/classy-store/tests/performance/performance.store.ts
@@ -0,0 +1,16 @@
+export class ScaleStore {
+ items: {id: number; text: string; completed: boolean}[];
+ unrelated = 0;
+
+ constructor(size: number) {
+ this.items = Array.from({length: size}, (_, index) => ({
+ id: index,
+ text: `Item ${index}`,
+ completed: false,
+ }));
+ }
+
+ setText(index: number, text: string): void {
+ this.items[index].text = text;
+ }
+}
diff --git a/packages/classy-store/tests/performance/performance.types.ts b/packages/classy-store/tests/performance/performance.types.ts
new file mode 100644
index 0000000..67cd49a
--- /dev/null
+++ b/packages/classy-store/tests/performance/performance.types.ts
@@ -0,0 +1,47 @@
+import type {ScaleStore} from './performance.store';
+
+export type ScaleConfiguration = {
+ workload: 'form' | 'list';
+ mode: 'selector' | 'tracked';
+ size: number;
+};
+
+export type ScaleOperation =
+ | 'change'
+ | 'burst'
+ | 'unrelated'
+ | 'noop'
+ | 'parent';
+
+export type ScaleConsumerProps = {
+ store: ScaleStore;
+ index: number;
+ revision: number;
+};
+
+export type ScaleMetrics = {
+ consumerCommits: number;
+ rootCommits: number;
+ selectorCalls: number;
+ renderDurationMs: number;
+ inputEventToCommitMs: number[];
+};
+
+export type ScaleMeasurement = {
+ durationsMs: number[];
+ metrics: ScaleMetrics;
+};
+
+export type ScaleBenchmark = {
+ mount: (configuration: ScaleConfiguration) => ScaleMeasurement;
+ measure: (operation: ScaleOperation, iterations: number) => ScaleMeasurement;
+ resetMetrics: () => void;
+ readMetrics: () => ScaleMetrics;
+ unmount: () => ScaleMetrics;
+};
+
+declare global {
+ interface Window {
+ scaleBenchmark: ScaleBenchmark;
+ }
+}
diff --git a/packages/classy-store/tests/performance/performanceMetrics.ts b/packages/classy-store/tests/performance/performanceMetrics.ts
new file mode 100644
index 0000000..853d74b
--- /dev/null
+++ b/packages/classy-store/tests/performance/performanceMetrics.ts
@@ -0,0 +1,48 @@
+import type {ProfilerOnRenderCallback} from 'react';
+import type {ScaleMetrics} from './performance.types';
+
+let metrics: ScaleMetrics;
+let inputStarted: number | undefined;
+
+export function resetMetrics(): void {
+ metrics = {
+ consumerCommits: 0,
+ rootCommits: 0,
+ selectorCalls: 0,
+ renderDurationMs: 0,
+ inputEventToCommitMs: [],
+ };
+ inputStarted = undefined;
+}
+
+resetMetrics();
+
+export function readMetrics(): ScaleMetrics {
+ return {...metrics, inputEventToCommitMs: [...metrics.inputEventToCommitMs]};
+}
+
+export function recordSelector(): void {
+ metrics.selectorCalls += 1;
+}
+
+// Effects count committed consumer executions; Compiler can cache render code.
+export function recordConsumerCommit(): void {
+ metrics.consumerCommits += 1;
+ if (inputStarted !== undefined) {
+ metrics.inputEventToCommitMs.push(performance.now() - inputStarted);
+ inputStarted = undefined;
+ }
+}
+
+export function recordInputStart(): void {
+ inputStarted = performance.now();
+}
+
+export const recordRootCommit: ProfilerOnRenderCallback = (
+ _id,
+ _phase,
+ actualDuration,
+) => {
+ metrics.rootCommits += 1;
+ metrics.renderDurationMs += actualDuration;
+};
diff --git a/packages/classy-store/tests/performance/runPerformance.ts b/packages/classy-store/tests/performance/runPerformance.ts
new file mode 100644
index 0000000..da0d36c
--- /dev/null
+++ b/packages/classy-store/tests/performance/runPerformance.ts
@@ -0,0 +1,301 @@
+import assert from 'node:assert/strict';
+import {mkdir} from 'node:fs/promises';
+import {arch, platform, release} from 'node:os';
+import {basename, dirname, resolve} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import {chromium} from '@playwright/test';
+import {compileConsumer} from '../compiler/compileConsumer.mjs';
+import type {
+ ScaleConfiguration,
+ ScaleMeasurement,
+ ScaleOperation,
+} from './performance.types';
+
+const directory = dirname(fileURLToPath(import.meta.url));
+const manifest = await Bun.file(
+ resolve(directory, '../../package.json'),
+).json();
+const dist = resolve(
+ process.env.BENCHMARK_DIST ?? resolve(directory, '../../dist'),
+);
+const output = resolve(
+ process.env.BENCHMARK_OUTPUT ??
+ resolve(directory, '../../../../test-results/react-performance.json'),
+);
+const sizes = (process.env.BENCHMARK_SIZES ?? '200,1000')
+ .split(',')
+ .map(Number);
+const samples = Number(process.env.BENCHMARK_SAMPLES ?? 5);
+const iterations = Number(process.env.BENCHMARK_ITERATIONS ?? 20);
+const warmup = 5;
+assert(sizes.every((size) => Number.isInteger(size) && size >= 10));
+assert(Number.isInteger(samples) && samples > 0);
+assert(Number.isInteger(iterations) && iterations > 0);
+
+const scripts = new Map();
+const compilation: {filename: string; compiledFunctions: unknown[]}[] = [];
+for (const compiler of [false, true]) {
+ const bundle = await Bun.build({
+ entrypoints: [resolve(directory, 'client.tsx')],
+ target: 'browser',
+ format: 'iife',
+ minify: true,
+ define: {'process.env.NODE_ENV': '"production"'},
+ plugins: [
+ {
+ name: 'scale-consumer',
+ setup(build) {
+ // Opt into React's production profiling renderer for both variants.
+ build.onResolve({filter: /^react-dom\/client$/}, () => ({
+ path: Bun.resolveSync('react-dom/profiling', directory),
+ }));
+ build.onResolve(
+ {filter: /^(?:react(?:\/.*)?|react-dom|proxy-compare)$/},
+ ({path}) => ({
+ path: Bun.resolveSync(path, directory),
+ }),
+ );
+ build.onResolve(
+ {filter: /^@codebelt\/classy-store(?:\/react)?$/},
+ ({path}) => ({
+ path: resolve(
+ dist,
+ path.endsWith('/react')
+ ? 'frameworks/react/react.mjs'
+ : 'index.mjs',
+ ),
+ }),
+ );
+ if (compiler) {
+ build.onLoad(
+ {filter: /\/performance\/components\/.*\.tsx$/},
+ async ({path}) => {
+ const result = compileConsumer(
+ await Bun.file(path).text(),
+ path,
+ '19',
+ [basename(path, '.tsx')],
+ );
+ compilation.push({
+ filename: basename(path),
+ compiledFunctions: result.compiledFunctions,
+ });
+ return {contents: result.code, loader: 'tsx'};
+ },
+ );
+ }
+ },
+ },
+ ],
+ });
+ if (!bundle.success)
+ throw new AggregateError(bundle.logs, 'Performance consumer build failed');
+ scripts.set(
+ compiler ? '/compiled.js' : '/plain.js',
+ await bundle.outputs[0].text(),
+ );
+}
+
+const server = Bun.serve({
+ hostname: '127.0.0.1',
+ port: 0,
+ fetch(request) {
+ const url = new URL(request.url);
+ const script = scripts.get(url.pathname);
+ if (script)
+ return new Response(script, {
+ headers: {'Content-Type': 'text/javascript'},
+ });
+ const scriptPath =
+ url.searchParams.get('compiler') === 'on' ? '/compiled.js' : '/plain.js';
+ return new Response(
+ `React scale benchmark
`,
+ {headers: {'Content-Type': 'text/html'}},
+ );
+ },
+});
+
+function summarize(values: number[]) {
+ const sorted = [...values].sort((left, right) => left - right);
+ return {
+ medianMs: sorted[Math.floor(sorted.length / 2)],
+ p95Ms: sorted[Math.ceil(sorted.length * 0.95) - 1],
+ minMs: sorted[0],
+ maxMs: sorted.at(-1),
+ };
+}
+
+const browser = await chromium.launch();
+const results: unknown[] = [];
+try {
+ for (const size of sizes) {
+ for (const workload of ['form', 'list'] as const) {
+ for (const mode of ['selector', 'tracked'] as const) {
+ for (const compiler of ['off', 'on'] as const) {
+ const configuration: ScaleConfiguration = {size, workload, mode};
+ const measurements: Record = {};
+ for (let sample = 0; sample < samples; sample += 1) {
+ const page = await browser.newPage();
+ const errors: string[] = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ page.on('console', (message) => {
+ if (message.type() === 'error') errors.push(message.text());
+ });
+ await page.goto(`${server.url}?compiler=${compiler}`);
+ await page
+ .waitForFunction(() => Boolean(window.scaleBenchmark))
+ .catch((error) => {
+ throw new AggregateError(
+ [error, ...errors],
+ `Benchmark failed to load (${compiler})`,
+ );
+ });
+ const mount = await page.evaluate(
+ (config) => window.scaleBenchmark.mount(config),
+ configuration,
+ );
+ assert.equal(
+ mount.metrics.consumerCommits,
+ size,
+ 'Every consumer must mount',
+ );
+ assert.equal(
+ mount.metrics.rootCommits,
+ 1,
+ 'Profiling renderer must be active',
+ );
+ measurements.mount ??= [];
+ measurements.mount.push(mount);
+ for (const operation of [
+ 'change',
+ 'burst',
+ 'unrelated',
+ 'noop',
+ 'parent',
+ ] as ScaleOperation[]) {
+ await page.evaluate(
+ ({operation, warmup}) =>
+ window.scaleBenchmark.measure(operation, warmup),
+ {operation, warmup},
+ );
+ const measurement = await page.evaluate(
+ ({operation, iterations}) =>
+ window.scaleBenchmark.measure(operation, iterations),
+ {operation, iterations},
+ );
+ const perIteration =
+ operation === 'parent'
+ ? size
+ : operation === 'burst'
+ ? 10
+ : operation === 'change'
+ ? 1
+ : 0;
+ assert.equal(
+ measurement.metrics.consumerCommits,
+ perIteration * iterations,
+ `${workload}/${mode}/${compiler}/${operation}: unexpected consumer commits`,
+ );
+ assert.equal(
+ measurement.metrics.rootCommits,
+ perIteration ? iterations : 0,
+ `${operation}: unexpected root commits`,
+ );
+ measurements[operation] ??= [];
+ measurements[operation].push(measurement);
+ }
+ if (workload === 'form') {
+ const input = page.getByRole('textbox', {
+ name: 'Field 0',
+ exact: true,
+ });
+ await input.focus();
+ await input.evaluate((element) =>
+ (element as HTMLInputElement).setSelectionRange(2, 2),
+ );
+ const previousValue = await input.inputValue();
+ await page.evaluate(() => window.scaleBenchmark.resetMetrics());
+ await input.pressSequentially('abcdefghij');
+ assert.equal(
+ await input.inputValue(),
+ `${previousValue.slice(0, 2)}abcdefghij${previousValue.slice(2)}`,
+ );
+ assert.equal(
+ await input.evaluate(
+ (element) => (element as HTMLInputElement).selectionStart,
+ ),
+ 12,
+ );
+ const metrics = await page.evaluate(() =>
+ window.scaleBenchmark.readMetrics(),
+ );
+ assert.equal(metrics.consumerCommits, 10);
+ assert.equal(metrics.inputEventToCommitMs.length, 10);
+ measurements.input ??= [];
+ measurements.input.push({
+ durationsMs: metrics.inputEventToCommitMs,
+ metrics,
+ });
+ }
+ const cleanup = await page.evaluate(() =>
+ window.scaleBenchmark.unmount(),
+ );
+ assert.equal(cleanup.consumerCommits, 0);
+ assert.equal(cleanup.selectorCalls, 0);
+ assert.deepEqual(errors, []);
+ await page.close();
+ }
+ const summary = Object.fromEntries(
+ Object.entries(measurements).map(([operation, entries]) => [
+ operation,
+ summarize(entries.flatMap((entry) => entry.durationsMs)),
+ ]),
+ );
+ results.push({
+ configuration,
+ compiler,
+ summary,
+ samples: measurements,
+ });
+ console.log(
+ `${size} ${workload} ${mode} compiler=${compiler}: mount ${summary.mount.medianMs.toFixed(2)}ms, change ${summary.change.medianMs.toFixed(2)}ms, parent ${summary.parent.medianMs.toFixed(2)}ms`,
+ );
+ }
+ }
+ }
+ }
+ await mkdir(dirname(output), {recursive: true});
+ await Bun.write(
+ output,
+ JSON.stringify(
+ {
+ metadata: {
+ createdAt: new Date().toISOString(),
+ react: manifest.devDependencies.react,
+ libraryVersion: manifest.version,
+ label: process.env.BENCHMARK_LABEL ?? 'working-tree',
+ compiler: manifest.devDependencies['babel-plugin-react-compiler'],
+ browser: browser.version(),
+ bun: Bun.version,
+ os: `${platform()} ${release()} ${arch()}`,
+ build: 'production profiling',
+ dist,
+ sizes,
+ samples,
+ iterations,
+ warmup,
+ notes:
+ 'Instrumented measurements; flushSync includes notification, snapshot, render and commit. Input measures input-event capture to consumer layout effect, not paint. Timing values are observations, never CI thresholds.',
+ },
+ compilation,
+ results,
+ },
+ null,
+ 2,
+ ),
+ );
+ console.log(`Saved raw samples and compilation evidence to ${output}`);
+} finally {
+ await browser.close();
+ server.stop(true);
+}
diff --git a/packages/classy-store/tests/performance/tsconfig.json b/packages/classy-store/tests/performance/tsconfig.json
new file mode 100644
index 0000000..8ad0b3c
--- /dev/null
+++ b/packages/classy-store/tests/performance/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {"rootDir": "../.."},
+ "include": ["**/*.ts", "**/*.tsx"]
+}
diff --git a/packages/classy-store/tests/scoping/README.md b/packages/classy-store/tests/scoping/README.md
new file mode 100644
index 0000000..08ce068
--- /dev/null
+++ b/packages/classy-store/tests/scoping/README.md
@@ -0,0 +1,33 @@
+# Scoped React context checks
+
+These tests exercise the editor context example in
+`examples/react/src/demos/scoped-store/store` against the published package shape.
+The compatibility runner copies that directory unchanged into each temporary
+consumer as `example/`, alongside these fixtures and the React-version-aware
+`reactAct.ts` helper. The `./example/...` imports intentionally resolve only in
+those consumers; there is no second implementation of the example in this folder.
+
+From the repository root:
+
+```sh
+bun run test:react:compatibility
+```
+
+The default matrix covers React 18.0, 18.3, and the library's pinned React version.
+See `../compatibility/README.md` for selected versions and retained consumers.
+
+- `scoping.lifecycle.test.tsx` checks independent providers, nested lookup,
+ stable stores, explicit keyed resets, missing-provider errors, and cleanup.
+- `scoping.ssr.test.tsx` checks request isolation, nested server contexts,
+ matching hydration, and persistence updates before a suspended consumer's
+ first hook read. Selector and auto-tracked consumers must retain the server
+ DOM and then catch up to the persisted state.
+- `scoping.qa.test.tsx` independently checks pending persistence reads during
+ Strict Mode replay, provider replacement, keyed resets, and unmount, as well
+ as abandoned renders and memoized consumers.
+- `scoping.types.fixture.tsx` checks the copied example's inferred store,
+ mutable actions, readonly snapshots, and provider props. The runner also
+ typechecks every scoping runtime test and its helper components.
+
+These are application ownership patterns built from existing public APIs;
+they do not add a context abstraction to the library.
diff --git a/packages/classy-store/tests/scoping/lifecycleStorage.ts b/packages/classy-store/tests/scoping/lifecycleStorage.ts
new file mode 100644
index 0000000..64f6485
--- /dev/null
+++ b/packages/classy-store/tests/scoping/lifecycleStorage.ts
@@ -0,0 +1,24 @@
+import type {StorageAdapter} from '@codebelt/classy-store/utils';
+
+export function createLifecycleStorage() {
+ const values = new Map();
+ const reads: string[] = [];
+ const writes: {name: string; value: string}[] = [];
+ const removals: string[] = [];
+ const storage: StorageAdapter = {
+ getItem(name) {
+ reads.push(name);
+ return values.get(name) ?? null;
+ },
+ setItem(name, value) {
+ writes.push({name, value});
+ values.set(name, value);
+ },
+ removeItem(name) {
+ removals.push(name);
+ values.delete(name);
+ },
+ };
+
+ return {storage, values, reads, writes, removals};
+}
diff --git a/packages/classy-store/tests/scoping/scoping.lifecycle.test.tsx b/packages/classy-store/tests/scoping/scoping.lifecycle.test.tsx
new file mode 100644
index 0000000..3384de3
--- /dev/null
+++ b/packages/classy-store/tests/scoping/scoping.lifecycle.test.tsx
@@ -0,0 +1,371 @@
+import {afterEach, describe, expect, test} from 'bun:test';
+import {type ReactNode, StrictMode} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {renderToString} from 'react-dom/server';
+import {EditorStoreProvider} from './example/components/editorStoreProvider/EditorStoreProvider';
+import type {EditorStore} from './example/editorStore';
+import {createLifecycleStorage} from './lifecycleStorage';
+import {act} from './reactAct';
+import {ScopedProbe} from './testHelpers/scopedProbe/ScopedProbe';
+
+const mountedRoots = new Map();
+const firstData = {title: 'First', body: 'First body'};
+const secondData = {title: 'Second', body: 'Second body'};
+
+async function mount(element: ReactNode) {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mountedRoots.set(root, container);
+ await act(async () => root.render(element));
+ return {root, container};
+}
+
+async function unmount(root: Root) {
+ const container = mountedRoots.get(root);
+ await act(async () => root.unmount());
+ container?.remove();
+ mountedRoots.delete(root);
+}
+
+function readScope(container: HTMLElement, name: string) {
+ return container.querySelector(`[data-scope="${name}"]`)?.textContent;
+}
+
+function readRequired(values: Map, name: string): Value {
+ const value = values.get(name);
+ if (value === undefined) throw new Error(`Missing captured value: ${name}`);
+ return value;
+}
+
+afterEach(async () => {
+ for (const root of mountedRoots.keys()) await unmount(root);
+});
+
+describe('typed context store lifecycle', () => {
+ test('isolates sibling instances and their persistence namespaces', async () => {
+ const {storage, writes, values} = createLifecycleStorage();
+ const stores = new Map();
+ const {container} = await mount(
+ <>
+
+ stores.set('first', store)}
+ />
+
+
+ stores.set('second', store)}
+ />
+
+ >,
+ );
+ const first = readRequired(stores, 'first');
+ const second = readRequired(stores, 'second');
+ expect(first).not.toBe(second);
+ expect(readScope(container, 'first')).toBe(
+ 'First:First body|First:First body',
+ );
+ expect(readScope(container, 'second')).toBe(
+ 'Second:Second body|Second:Second body',
+ );
+
+ await act(async () => {
+ first.setTitle('Edited first');
+ first.setBody('Edited body');
+ });
+ expect(readScope(container, 'first')).toBe(
+ 'Edited first:Edited body|Edited first:Edited body',
+ );
+ expect(readScope(container, 'second')).toBe(
+ 'Second:Second body|Second:Second body',
+ );
+ expect(writes.map(({name}) => name)).toEqual(['editor:first']);
+
+ await act(async () => second.setTitle('Edited second'));
+ expect(readScope(container, 'first')).toBe(
+ 'Edited first:Edited body|Edited first:Edited body',
+ );
+ expect(readScope(container, 'second')).toBe(
+ 'Edited second:Second body|Edited second:Second body',
+ );
+ expect(JSON.parse(readRequired(values, 'editor:first')).state).toEqual({
+ title: 'Edited first',
+ body: 'Edited body',
+ });
+ expect(JSON.parse(readRequired(values, 'editor:second')).state).toEqual({
+ title: 'Edited second',
+ body: 'Second body',
+ });
+ });
+
+ test('routes nested consumers to the nearest provider and restores the outer scope', async () => {
+ const stores = new Map();
+ const {container} = await mount(
+
+ stores.set('before', store)}
+ />
+
+ stores.set('inside', store)}
+ />
+
+ stores.set('after', store)}
+ />
+ ,
+ );
+ expect(stores.get('before')).toBe(stores.get('after'));
+ expect(stores.get('inside')).not.toBe(stores.get('before'));
+
+ await act(async () =>
+ readRequired(stores, 'inside').setTitle('Nested edit'),
+ );
+ expect(readScope(container, 'before')).toBe(
+ 'First:First body|First:First body',
+ );
+ expect(readScope(container, 'inside')).toBe(
+ 'Nested edit:Second body|Nested edit:Second body',
+ );
+ expect(readScope(container, 'after')).toBe(
+ 'First:First body|First:First body',
+ );
+
+ await act(async () => readRequired(stores, 'before').setBody('Outer edit'));
+ expect(readScope(container, 'before')).toBe(
+ 'First:Outer edit|First:Outer edit',
+ );
+ expect(readScope(container, 'inside')).toBe(
+ 'Nested edit:Second body|Nested edit:Second body',
+ );
+ expect(readScope(container, 'after')).toBe(
+ 'First:Outer edit|First:Outer edit',
+ );
+ });
+
+ test('reports a clear missing-provider error', () => {
+ expect(() => renderToString( )).toThrow(
+ 'useEditorStore must be used within EditorStoreProvider',
+ );
+ });
+
+ test('preserves the store and edits when parent renders pass new initial data', async () => {
+ const captures: EditorStore[] = [];
+ const capture = (store: EditorStore) => captures.push(store);
+ const {root, container} = await mount(
+
+
+ ,
+ );
+ const store = captures[0];
+ await act(async () => store.setTitle('Unsaved edit'));
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(captures).toEqual([store]);
+ expect(readScope(container, 'editor')).toBe(
+ 'Unsaved edit:First body|Unsaved edit:First body',
+ );
+ await act(async () => store.setBody('Still connected'));
+ expect(readScope(container, 'editor')).toBe(
+ 'Unsaved edit:Still connected|Unsaved edit:Still connected',
+ );
+ });
+
+ test('resets with a provider key and disconnects the previous store', async () => {
+ const {storage, reads, writes} = createLifecycleStorage();
+ const captures: EditorStore[] = [];
+ const commits: string[] = [];
+ const capture = (store: EditorStore) => captures.push(store);
+ const commit = (value: string) => commits.push(value);
+ const {root, container} = await mount(
+
+
+ ,
+ );
+ const previousStore = captures[0];
+ await act(async () => previousStore.setTitle('Old edit'));
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ const currentStore = captures[1];
+ expect(currentStore).not.toBe(previousStore);
+ expect(readScope(container, 'editor')).toBe(
+ 'Second:Second body|Second:Second body',
+ );
+ expect(reads).toEqual(['first', 'second']);
+ writes.length = 0;
+ commits.length = 0;
+ await act(async () => previousStore.setTitle('Detached edit'));
+ expect(writes).toEqual([]);
+ expect(commits).toEqual([]);
+ expect(readScope(container, 'editor')).toBe(
+ 'Second:Second body|Second:Second body',
+ );
+ await act(async () => currentStore.setBody('Current edit'));
+ expect(readScope(container, 'editor')).toBe(
+ 'Second:Current edit|Second:Current edit',
+ );
+ expect(writes.map(({name}) => name)).toEqual(['second']);
+ });
+
+ test('maintains one active persistence effect through StrictMode replay and unmount', async () => {
+ const {storage, reads, writes} = createLifecycleStorage();
+ const captures: EditorStore[] = [];
+ const {root, container} = await mount(
+
+
+ captures.push(store)}
+ />
+
+ ,
+ );
+ expect(reads).toEqual(['strict', 'strict']);
+ expect(new Set(captures).size).toBe(1);
+ const store = captures[0];
+ await act(async () => store.setTitle('Strict edit'));
+ expect(readScope(container, 'editor')).toBe(
+ 'Strict edit:First body|Strict edit:First body',
+ );
+ expect(writes).toHaveLength(1);
+ await unmount(root);
+ writes.length = 0;
+ await act(async () => store.setBody('After unmount'));
+ expect(writes).toEqual([]);
+ });
+
+ test('does not restart persistence for an equivalent options object', async () => {
+ const {storage, reads, writes} = createLifecycleStorage();
+ const stores: EditorStore[] = [];
+ const capture = (store: EditorStore) => stores.push(store);
+ const {root, container} = await mount(
+
+
+ ,
+ );
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(reads).toEqual(['stable']);
+ expect(stores).toHaveLength(1);
+ await act(async () => stores[0].setBody('Edited'));
+ expect(writes).toHaveLength(1);
+ expect(readScope(container, 'editor')).toBe('First:Edited|First:Edited');
+ });
+
+ test('cleans up when persistence is removed and reconnects without recreating the store', async () => {
+ const {storage, reads, writes} = createLifecycleStorage();
+ const stores: EditorStore[] = [];
+ const capture = (store: EditorStore) => stores.push(store);
+ const {root, container} = await mount(
+
+
+ ,
+ );
+ const store = stores[0];
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await act(async () => store.setTitle('Temporary edit'));
+ expect(writes).toEqual([]);
+ expect(readScope(container, 'editor')).toBe(
+ 'Temporary edit:First body|Temporary edit:First body',
+ );
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(stores).toEqual([store]);
+ expect(reads).toEqual(['before', 'after']);
+ await act(async () => store.setBody('Reconnected'));
+ expect(writes.map(({name}) => name)).toEqual(['after']);
+ });
+
+ test('a keyed remount restores saved data when the persistence namespace stays the same', async () => {
+ const {storage, removals} = createLifecycleStorage();
+ const stores: EditorStore[] = [];
+ const capture = (store: EditorStore) => stores.push(store);
+ const {root, container} = await mount(
+
+
+ ,
+ );
+ await act(async () => stores[0].setTitle('Saved edit'));
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(stores[1]).not.toBe(stores[0]);
+ expect(removals).toEqual([]);
+ expect(readScope(container, 'editor')).toBe(
+ 'Saved edit:First body|Saved edit:First body',
+ );
+ });
+});
diff --git a/packages/classy-store/tests/scoping/scoping.qa.test.tsx b/packages/classy-store/tests/scoping/scoping.qa.test.tsx
new file mode 100644
index 0000000..8ee04e6
--- /dev/null
+++ b/packages/classy-store/tests/scoping/scoping.qa.test.tsx
@@ -0,0 +1,238 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import type {StorageAdapter} from '@codebelt/classy-store/utils';
+import {type ReactNode, StrictMode, Suspense} from 'react';
+import {createRoot, type Root} from 'react-dom/client';
+import {EditorStoreProvider} from './example/components/editorStoreProvider/EditorStoreProvider';
+import type {EditorStore} from './example/editorStore';
+import {act} from './reactAct';
+import {QaMemoSelector} from './testHelpers/scopedQa/qaMemoSelector/QaMemoSelector';
+import {QaScopeProbe} from './testHelpers/scopedQa/qaScopeProbe/QaScopeProbe';
+
+const mounted = new Map();
+const initialData = {title: 'Initial', body: 'Body'};
+
+function mount(element: ReactNode) {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ mounted.set(root, container);
+ act(() => root.render(element));
+ return {root, container};
+}
+
+function unmount(root: Root) {
+ act(() => root.unmount());
+ mounted.get(root)?.remove();
+ mounted.delete(root);
+}
+
+function createPendingStorage() {
+ const reads: {name: string; resolve: (value: string | null) => void}[] = [];
+ const storage: StorageAdapter = {
+ getItem: mock(
+ (name: string) =>
+ new Promise((resolve) => {
+ reads.push({name, resolve});
+ }),
+ ),
+ setItem: mock((_name: string, _value: string) => {}),
+ removeItem: mock((_name: string) => {}),
+ };
+ return {storage, reads};
+}
+
+function saved(title: string) {
+ return JSON.stringify({version: 0, state: {title, body: 'Saved body'}});
+}
+
+afterEach(() => {
+ for (const root of mounted.keys()) unmount(root);
+});
+
+describe('independent scoped-store lifecycle QA', () => {
+ test('ignores a late hydration result from the disposed first StrictMode effect', async () => {
+ const {storage, reads} = createPendingStorage();
+ let currentStore!: EditorStore;
+ const onStore = (store: EditorStore) => {
+ currentStore = store;
+ };
+ const {container} = mount(
+
+
+
+
+ ,
+ );
+ expect(reads).toHaveLength(2);
+ await act(async () => reads[1]?.resolve(saved('Current')));
+ expect(container.textContent).toBe('Current|Saved body');
+ await act(async () => reads[0]?.resolve(saved('Stale')));
+ expect(currentStore.title).toBe('Current');
+ expect(container.textContent).toBe('Current|Saved body');
+ expect(storage.setItem).not.toHaveBeenCalled();
+
+ await act(async () => currentStore.setTitle('User edit'));
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+ expect(container.textContent).toBe('User edit|Saved body');
+ });
+
+ test('ignores a pending hydration after a storage key changes on the same store', async () => {
+ const {storage, reads} = createPendingStorage();
+ let currentStore!: EditorStore;
+ const onStore = (store: EditorStore) => {
+ currentStore = store;
+ };
+ const view = (name: string) => (
+
+
+
+ );
+ const {root, container} = mount(view('previous'));
+ const originalStore = currentStore;
+ act(() => root.render(view('current')));
+ expect(currentStore).toBe(originalStore);
+ expect(reads.map((read) => read.name)).toEqual(['previous', 'current']);
+ await act(async () => reads[1]?.resolve(saved('Current')));
+ await act(async () => reads[0]?.resolve(saved('Previous')));
+ expect(container.textContent).toBe('Current|Saved body');
+ expect(storage.setItem).not.toHaveBeenCalled();
+ await act(async () => currentStore.setBody('Changed'));
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+ expect(storage.setItem).toHaveBeenCalledWith(
+ 'current',
+ JSON.stringify({
+ version: 0,
+ state: {title: 'Current', body: 'Changed'},
+ }),
+ );
+ });
+
+ test('keeps a reset store and its retired predecessor isolated from late persistence', async () => {
+ const {storage, reads} = createPendingStorage();
+ let currentStore!: EditorStore;
+ const onStore = (store: EditorStore) => {
+ currentStore = store;
+ };
+ const view = (session: string) => (
+
+
+
+ );
+ const {root, container} = mount(view('previous'));
+ const retiredStore = currentStore;
+ act(() => root.render(view('current')));
+ expect(currentStore).not.toBe(retiredStore);
+ await act(async () => reads[1]?.resolve(saved('Current')));
+ await act(async () => reads[0]?.resolve(saved('Retired')));
+ expect(retiredStore.title).toBe('Initial');
+ expect(container.textContent).toBe('Current|Saved body');
+ expect(storage.setItem).not.toHaveBeenCalled();
+ await act(async () => retiredStore.setTitle('Detached edit'));
+ expect(container.textContent).toBe('Current|Saved body');
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ test('does not mutate a disposed store when its pending hydration completes after unmount', async () => {
+ const {storage, reads} = createPendingStorage();
+ let retiredStore!: EditorStore;
+ const {root} = mount(
+
+ {
+ retiredStore = store;
+ }}
+ />
+ ,
+ );
+ expect(reads).toHaveLength(1);
+ unmount(root);
+ await act(async () => reads[0]?.resolve(saved('Too late')));
+ expect(retiredStore.title).toBe('Initial');
+ expect(retiredStore.body).toBe('Body');
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ test('never starts persistence for an initial render abandoned while suspended', async () => {
+ const {storage} = createPendingStorage();
+ let released = false;
+ let release!: () => void;
+ const promise = new Promise((resolve) => {
+ release = resolve;
+ });
+ const onStore = mock((_store: EditorStore) => {});
+ const suspend = () => {
+ if (!released) throw promise;
+ };
+ const {root, container} = mount(
+
+
+
+
+ ,
+ );
+ expect(container.textContent).toBe('Loading');
+ expect(onStore).not.toHaveBeenCalled();
+ expect(storage.getItem).not.toHaveBeenCalled();
+ act(() => root.render(null));
+ await act(async () => {
+ released = true;
+ release();
+ });
+ expect(onStore).not.toHaveBeenCalled();
+ expect(storage.getItem).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ });
+
+ test('memoized selectors and tracked consumers stay live while provider props change', async () => {
+ let currentStore!: EditorStore;
+ const onStore = (store: EditorStore) => {
+ currentStore = store;
+ };
+ const view = (title: string) => (
+
+
+
+
+ );
+ const {root, container} = mount(view('Initial'));
+ const originalStore = currentStore;
+ act(() => root.render(view('Ignored new prop')));
+ expect(currentStore).toBe(originalStore);
+ expect(container.querySelector('[data-qa-tracked]')?.textContent).toBe(
+ 'Initial|Body',
+ );
+ expect(container.querySelector('[data-qa-selected]')?.textContent).toBe(
+ 'Initial',
+ );
+ await act(async () => currentStore.setTitle('Updated'));
+ expect(container.querySelector('[data-qa-tracked]')?.textContent).toBe(
+ 'Updated|Body',
+ );
+ expect(container.querySelector('[data-qa-selected]')?.textContent).toBe(
+ 'Updated',
+ );
+ await act(async () => currentStore.setBody('Updated body'));
+ expect(container.querySelector('[data-qa-tracked]')?.textContent).toBe(
+ 'Updated|Updated body',
+ );
+ expect(container.querySelector('[data-qa-selected]')?.textContent).toBe(
+ 'Updated',
+ );
+ });
+});
diff --git a/packages/classy-store/tests/scoping/scoping.ssr.test.tsx b/packages/classy-store/tests/scoping/scoping.ssr.test.tsx
new file mode 100644
index 0000000..0508169
--- /dev/null
+++ b/packages/classy-store/tests/scoping/scoping.ssr.test.tsx
@@ -0,0 +1,242 @@
+import {afterEach, describe, expect, mock, test} from 'bun:test';
+import {hydrateRoot, type Root} from 'react-dom/client';
+import {renderToString} from 'react-dom/server';
+import {EditorStoreProvider} from './example/components/editorStoreProvider/EditorStoreProvider';
+import type {EditorStore} from './example/editorStore';
+import {act} from './reactAct';
+import type {
+ HydrationGate,
+ ScopedSsrMode,
+} from './testHelpers/scopedSsr/scopedSsr.types';
+import {ScopedSsrProbe} from './testHelpers/scopedSsr/scopedSsrProbe/ScopedSsrProbe';
+import {ScopedSsrTree} from './testHelpers/scopedSsr/scopedSsrTree/ScopedSsrTree';
+import {ScopedSsrValue} from './testHelpers/scopedSsr/scopedSsrValue/ScopedSsrValue';
+
+function createGate(initiallyReady = false): HydrationGate {
+ let ready = initiallyReady;
+ let releasePromise = () => {};
+ const promise = new Promise((resolvePromise) => {
+ releasePromise = resolvePromise;
+ });
+ return {
+ read() {
+ if (!ready) throw promise;
+ },
+ release() {
+ ready = true;
+ releasePromise();
+ },
+ };
+}
+
+const modes: ScopedSsrMode[] = ['selector', 'auto-tracking'];
+const mounted = new Map();
+
+afterEach(async () => {
+ for (const [root, container] of mounted) {
+ await act(async () => root.unmount());
+ container.remove();
+ }
+ mounted.clear();
+});
+
+describe('scoped context server rendering and hydration', () => {
+ test('creates isolated request stores without starting persistence on the server', () => {
+ const stores: EditorStore[] = [];
+ const initialData = {title: 'Shared input', body: 'Original'};
+ const storage = {
+ getItem: mock(() => null),
+ setItem: mock(() => {}),
+ removeItem: mock(() => {}),
+ };
+ const element = (
+ stores.push(store)}
+ />
+ );
+ const firstHtml = renderToString(element);
+ stores[0].setTitle('Changed first request');
+ stores[0].setBody('Private first request');
+ const secondHtml = renderToString(element);
+
+ expect(secondHtml).toBe(firstHtml);
+ expect(stores).toHaveLength(2);
+ expect(stores[0]).not.toBe(stores[1]);
+ expect(stores[1].title).toBe('Shared input');
+ expect(stores[1].body).toBe('Original');
+ expect(initialData).toEqual({title: 'Shared input', body: 'Original'});
+ expect(storage.getItem).not.toHaveBeenCalled();
+ expect(storage.setItem).not.toHaveBeenCalled();
+ expect(storage.removeItem).not.toHaveBeenCalled();
+ });
+
+ test('resolves nested providers independently during server rendering', () => {
+ const outerStores: EditorStore[] = [];
+ const innerStores: EditorStore[] = [];
+ const outerHtml = renderToString(
+
+ outerStores.push(store)} />
+
+
+ innerStores.push(store)} />
+
+
+ outerStores.push(store)} />
+ ,
+ );
+ expect(outerHtml).toContain('Outer|Request');
+ expect(outerHtml).toContain('Inner|Draft');
+ expect(outerStores).toHaveLength(2);
+ expect(outerStores[0]).toBe(outerStores[1]);
+ expect(innerStores).toHaveLength(1);
+ expect(innerStores[0]).not.toBe(outerStores[0]);
+ innerStores[0].setTitle('Inner only');
+ expect(outerStores[0].title).toBe('Outer');
+ });
+
+ test.each(
+ modes,
+ )('hydrates matching %s context state without replacing server DOM', async (mode) => {
+ const serverStores: EditorStore[] = [];
+ const clientStores: EditorStore[] = [];
+ const initialData = {title: 'Server', body: 'Request data'};
+ const container = document.createElement('div');
+ container.innerHTML = renderToString(
+ serverStores.push(store)}
+ />,
+ );
+ document.body.appendChild(container);
+ const serverElement = container.querySelector('[data-scoped-value]');
+ const onRecoverableError = mock(() => {});
+ const onCommit = mock((value: string) => value);
+
+ await act(async () => {
+ const root = hydrateRoot(
+ container,
+ clientStores.push(store)}
+ onCommit={onCommit}
+ />,
+ {onRecoverableError},
+ );
+ mounted.set(root, container);
+ });
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.querySelector('[data-scoped-value]')).toBe(serverElement);
+ expect(serverElement?.textContent).toBe('Server|Request data');
+ expect(clientStores[0]).not.toBe(serverStores[0]);
+ expect(onCommit.mock.calls.map(([value]) => value)).toEqual([
+ 'Server|Request data',
+ ]);
+
+ await act(async () => {
+ clientStores[0].setTitle('Client edit');
+ clientStores[0].setBody('Updated body');
+ });
+ expect(serverElement?.textContent).toBe('Client edit|Updated body');
+ expect(serverStores[0].title).toBe('Server');
+ expect(serverStores[0].body).toBe('Request data');
+ });
+
+ test.each(
+ modes,
+ )('restores persistence before a delayed %s context consumer first hydrates', async (mode) => {
+ const initialData = {title: 'Server', body: 'Initial body'};
+ let serverStore!: EditorStore;
+ let clientStore!: EditorStore;
+ const container = document.createElement('div');
+ container.innerHTML = renderToString(
+ {
+ serverStore = store;
+ }}
+ />,
+ );
+ document.body.appendChild(container);
+ const serverElement = container.querySelector('[data-scoped-value]');
+ const gate = createGate();
+ const onRead = mock((value: string) => value);
+ const onCommit = mock((value: string) => value);
+ const onRecoverableError = mock(() => {});
+ const storage = {
+ getItem: mock(async () =>
+ JSON.stringify({
+ version: 0,
+ state: {title: 'Saved', body: 'Persisted body'},
+ }),
+ ),
+ setItem: mock(() => {}),
+ removeItem: mock(() => {}),
+ };
+ let root!: Root;
+ await act(async () => {
+ root = hydrateRoot(
+ container,
+ {
+ clientStore = store;
+ }}
+ onRead={onRead}
+ onCommit={onCommit}
+ />,
+ {onRecoverableError},
+ );
+ mounted.set(root, container);
+ });
+
+ // The provider owns persistence outside the suspended subtree. This proves
+ // restoration finished before the first context consumer's store read.
+ expect(storage.getItem).toHaveBeenCalledTimes(1);
+ expect(clientStore.title).toBe('Saved');
+ expect(clientStore.body).toBe('Persisted body');
+ expect(onRead).not.toHaveBeenCalled();
+ expect(onCommit).not.toHaveBeenCalled();
+ expect(serverElement?.textContent).toBe('Server|Initial body');
+ expect(container.querySelector('[data-scoped-value]')).toBe(serverElement);
+
+ await act(async () => gate.release());
+
+ expect(onRecoverableError).not.toHaveBeenCalled();
+ expect(container.querySelector('[data-scoped-value]')).toBe(serverElement);
+ expect(serverElement?.textContent).toBe('Saved|Persisted body');
+ expect(onRead.mock.calls[0][0]).toBe('Server|Initial body');
+ expect(onCommit.mock.calls.map(([value]) => value)).toEqual([
+ 'Server|Initial body',
+ 'Saved|Persisted body',
+ ]);
+ expect(storage.setItem).not.toHaveBeenCalled();
+ expect(serverStore.title).toBe('Server');
+ expect(serverStore.body).toBe('Initial body');
+
+ await act(async () => {
+ clientStore.setTitle('Edited');
+ clientStore.setBody('Client body');
+ });
+ expect(serverElement?.textContent).toBe('Edited|Client body');
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+
+ const commitsBeforeUnmount = onCommit.mock.calls.length;
+ await act(async () => root.unmount());
+ mounted.delete(root);
+ container.remove();
+ clientStore.setTitle('Detached');
+ await Promise.resolve();
+ expect(storage.setItem).toHaveBeenCalledTimes(1);
+ expect(onCommit).toHaveBeenCalledTimes(commitsBeforeUnmount);
+ });
+});
diff --git a/packages/classy-store/tests/scoping/scoping.types.fixture.tsx b/packages/classy-store/tests/scoping/scoping.types.fixture.tsx
new file mode 100644
index 0000000..394fe8a
--- /dev/null
+++ b/packages/classy-store/tests/scoping/scoping.types.fixture.tsx
@@ -0,0 +1,69 @@
+import type {Snapshot} from '@codebelt/classy-store';
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {ComponentProps} from 'react';
+import type {EditorStoreProvider} from './example/components/editorStoreProvider/EditorStoreProvider';
+import type {EditorStore} from './example/editorStore';
+import {useEditorStore} from './example/hooks/useEditorStore';
+
+type Equal =
+ (() => Value extends Left ? 1 : 2) extends <
+ Value,
+ >() => Value extends Right ? 1 : 2
+ ? true
+ : false;
+type Expect = Condition;
+type ProviderProps = ComponentProps;
+
+export function useScopedTypeAssertions() {
+ const store = useEditorStore();
+ const selected = useClassyStore(store, (state) => state.title);
+ const tracked = useClassyStore(store);
+ const assertions: [
+ Expect>,
+ Expect>,
+ Expect>>,
+ ] = [true, true, true];
+ store.setTitle('Typed title');
+ store.setBody('Typed body');
+ store.title = 'Mutable commands';
+
+ // @ts-expect-error The strict context hook never returns an optional store.
+ const missingStore: undefined = store;
+ // @ts-expect-error Commands retain their argument types.
+ store.setTitle(123);
+ // @ts-expect-error Auto-tracked snapshots stay readonly through context.
+ tracked.title = 'Readonly';
+ // @ts-expect-error Selectors receive readonly snapshots.
+ useClassyStore(store, (state) => (state.body = 'Readonly'));
+ // @ts-expect-error The selected field is inferred as a string.
+ const wrongSelection: number = selected;
+
+ return {assertions, missingStore, wrongSelection};
+}
+
+const validProps: ProviderProps = {
+ initialData: {title: 'Title', body: 'Body'},
+ children: null,
+};
+const missingBody: ProviderProps = {
+ // @ts-expect-error Initial data requires all editor fields.
+ initialData: {title: 'Title'},
+ children: null,
+};
+const missingStorage: ProviderProps = {
+ ...validProps,
+ // @ts-expect-error Persistence requires a storage adapter and key together.
+ persistence: {name: 'editor'},
+};
+const invalidInitialData: ProviderProps = {
+ // @ts-expect-error Initial fields retain their string types.
+ initialData: {title: 1, body: 'Body'},
+ children: null,
+};
+
+export const scopedProviderTypeAssertions = {
+ validProps,
+ missingBody,
+ missingStorage,
+ invalidInitialData,
+};
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedProbe/ScopedProbe.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedProbe/ScopedProbe.tsx
new file mode 100644
index 0000000..d63ea59
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedProbe/ScopedProbe.tsx
@@ -0,0 +1,29 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useEffect} from 'react';
+import type {EditorStore} from '../../example/editorStore';
+import {useEditorStore} from '../../example/hooks/useEditorStore';
+
+type Props = {
+ name: string;
+ onStore?: (store: EditorStore) => void;
+ onCommit?: (value: string) => void;
+};
+
+export function ScopedProbe({name, onStore, onCommit}: Props) {
+ const store = useEditorStore();
+ const tracked = useClassyStore(store);
+ const selected = useClassyStore(
+ store,
+ (state) => `${state.title}:${state.body}`,
+ );
+ const value = `${tracked.title}:${tracked.body}|${selected}`;
+
+ useEffect(() => {
+ onStore?.(store);
+ }, [onStore, store]);
+ useEffect(() => {
+ onCommit?.(value);
+ });
+
+ return {value} ;
+}
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedQa/qaMemoSelector/QaMemoSelector.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedQa/qaMemoSelector/QaMemoSelector.tsx
new file mode 100644
index 0000000..ce058e1
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedQa/qaMemoSelector/QaMemoSelector.tsx
@@ -0,0 +1,9 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {memo} from 'react';
+import {useEditorStore} from '../../../example/hooks/useEditorStore';
+
+export const QaMemoSelector = memo(function QaMemoSelector() {
+ const store = useEditorStore();
+ const title = useClassyStore(store, (state) => state.title);
+ return {title} ;
+});
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedQa/qaScopeProbe/QaScopeProbe.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedQa/qaScopeProbe/QaScopeProbe.tsx
new file mode 100644
index 0000000..4224938
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedQa/qaScopeProbe/QaScopeProbe.tsx
@@ -0,0 +1,24 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {memo, useEffect} from 'react';
+import type {EditorStore} from '../../../example/editorStore';
+import {useEditorStore} from '../../../example/hooks/useEditorStore';
+
+type Props = {
+ onStore: (store: EditorStore) => void;
+ suspend?: () => void;
+};
+
+export const QaScopeProbe = memo(function QaScopeProbe({
+ onStore,
+ suspend,
+}: Props) {
+ const store = useEditorStore();
+ const state = useClassyStore(store);
+ suspend?.();
+ useEffect(() => onStore(store), [store, onStore]);
+ return (
+
+ {state.title}|{state.body}
+
+ );
+});
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsr.types.ts b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsr.types.ts
new file mode 100644
index 0000000..3526efa
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsr.types.ts
@@ -0,0 +1,14 @@
+import type {EditorStore} from '../../example/editorStore';
+
+export type HydrationGate = {
+ read: () => void;
+ release: () => void;
+};
+
+export type ScopedSsrMode = 'selector' | 'auto-tracking';
+
+export type ScopedSsrObservation = {
+ onStore?: (store: EditorStore) => void;
+ onRead?: (value: string) => void;
+ onCommit?: (value: string) => void;
+};
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrProbe/ScopedSsrProbe.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrProbe/ScopedSsrProbe.tsx
new file mode 100644
index 0000000..88fd558
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrProbe/ScopedSsrProbe.tsx
@@ -0,0 +1,10 @@
+import {useEditorStore} from '../../../example/hooks/useEditorStore';
+import type {ScopedSsrObservation} from '../scopedSsr.types';
+
+type Props = Pick;
+
+export function ScopedSsrProbe({onStore}: Props) {
+ const store = useEditorStore();
+ onStore?.(store);
+ return null;
+}
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrSelectedValue/ScopedSsrSelectedValue.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrSelectedValue/ScopedSsrSelectedValue.tsx
new file mode 100644
index 0000000..c3793f8
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrSelectedValue/ScopedSsrSelectedValue.tsx
@@ -0,0 +1,19 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useEffect} from 'react';
+import {useEditorStore} from '../../../example/hooks/useEditorStore';
+import type {ScopedSsrObservation} from '../scopedSsr.types';
+
+type Props = Pick;
+
+export function ScopedSsrSelectedValue({onRead, onCommit}: Props) {
+ const store = useEditorStore();
+ const value = useClassyStore(
+ store,
+ (state) => `${state.title}|${state.body}`,
+ );
+ onRead?.(value);
+ useEffect(() => {
+ onCommit?.(value);
+ }, [onCommit, value]);
+ return {value} ;
+}
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrTrackedValue/ScopedSsrTrackedValue.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrTrackedValue/ScopedSsrTrackedValue.tsx
new file mode 100644
index 0000000..435f619
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrTrackedValue/ScopedSsrTrackedValue.tsx
@@ -0,0 +1,17 @@
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useEffect} from 'react';
+import {useEditorStore} from '../../../example/hooks/useEditorStore';
+import type {ScopedSsrObservation} from '../scopedSsr.types';
+
+type Props = Pick;
+
+export function ScopedSsrTrackedValue({onRead, onCommit}: Props) {
+ const store = useEditorStore();
+ const state = useClassyStore(store);
+ const value = `${state.title}|${state.body}`;
+ onRead?.(value);
+ useEffect(() => {
+ onCommit?.(value);
+ }, [onCommit, value]);
+ return {value} ;
+}
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrTree/ScopedSsrTree.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrTree/ScopedSsrTree.tsx
new file mode 100644
index 0000000..3f8c094
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrTree/ScopedSsrTree.tsx
@@ -0,0 +1,45 @@
+import type {StorageAdapter} from '@codebelt/classy-store/utils';
+import {Suspense} from 'react';
+import {EditorStoreProvider} from '../../../example/components/editorStoreProvider/EditorStoreProvider';
+import type {EditorData} from '../../../example/editorStore';
+import type {
+ HydrationGate,
+ ScopedSsrMode,
+ ScopedSsrObservation,
+} from '../scopedSsr.types';
+import {ScopedSsrProbe} from '../scopedSsrProbe/ScopedSsrProbe';
+import {ScopedSsrValue} from '../scopedSsrValue/ScopedSsrValue';
+
+type Props = ScopedSsrObservation & {
+ initialData: EditorData;
+ gate?: HydrationGate;
+ mode: ScopedSsrMode;
+ persistence?: {name: string; storage: StorageAdapter};
+};
+
+export function ScopedSsrTree({
+ initialData,
+ gate,
+ mode,
+ persistence,
+ onStore,
+ onRead,
+ onCommit,
+}: Props) {
+ return (
+
+
+
+ Request shell
+ Loading editor}>
+
+
+
+
+ );
+}
diff --git a/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrValue/ScopedSsrValue.tsx b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrValue/ScopedSsrValue.tsx
new file mode 100644
index 0000000..195a88f
--- /dev/null
+++ b/packages/classy-store/tests/scoping/testHelpers/scopedSsr/scopedSsrValue/ScopedSsrValue.tsx
@@ -0,0 +1,21 @@
+import type {
+ HydrationGate,
+ ScopedSsrMode,
+ ScopedSsrObservation,
+} from '../scopedSsr.types';
+import {ScopedSsrSelectedValue} from '../scopedSsrSelectedValue/ScopedSsrSelectedValue';
+import {ScopedSsrTrackedValue} from '../scopedSsrTrackedValue/ScopedSsrTrackedValue';
+
+type Props = ScopedSsrObservation & {
+ gate?: HydrationGate;
+ mode: ScopedSsrMode;
+};
+
+export function ScopedSsrValue({gate, mode, onRead, onCommit}: Props) {
+ gate?.read();
+ return mode === 'selector' ? (
+
+ ) : (
+
+ );
+}
diff --git a/packages/classy-store/tsconfig.json b/packages/classy-store/tsconfig.json
index 4b917ae..2eebc0f 100644
--- a/packages/classy-store/tsconfig.json
+++ b/packages/classy-store/tsconfig.json
@@ -3,6 +3,7 @@
"module": "esnext",
"moduleResolution": "bundler",
"target": "es2022",
+ "types": ["bun"],
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
diff --git a/packages/classy-store/tsdown.config.ts b/packages/classy-store/tsdown.config.ts
index 4204db7..4abfb5a 100644
--- a/packages/classy-store/tsdown.config.ts
+++ b/packages/classy-store/tsdown.config.ts
@@ -5,6 +5,7 @@ export default defineConfig({
'src/index.ts',
'src/collections/index.ts',
'src/frameworks/react/react.ts',
+ 'src/frameworks/react/server.ts',
'src/frameworks/vue/vue.ts',
'src/frameworks/svelte/svelte.ts',
'src/frameworks/angular/angular.ts',
@@ -16,12 +17,14 @@ export default defineConfig({
sourcemap: true,
clean: true,
treeshake: true,
- external: [
- 'react',
- 'react-dom',
- 'vue',
- 'svelte',
- '@angular/core',
- 'solid-js',
- ],
+ deps: {
+ neverBundle: [
+ 'react',
+ 'react-dom',
+ 'vue',
+ 'svelte',
+ '@angular/core',
+ 'solid-js',
+ ],
+ },
});
diff --git a/website/docs/ARCHITECTURE.md b/website/docs/ARCHITECTURE.md
index 0176b1e..5af591b 100644
--- a/website/docs/ARCHITECTURE.md
+++ b/website/docs/ARCHITECTURE.md
@@ -120,7 +120,7 @@ PERSIST_ARCHITECTURE.md # Persist utility internals
### Overview
-The `createClassyStore()` function wraps a class instance in an ES6 Proxy. All mutations β property writes, array operations, nested object changes β are intercepted. Subscribers are batched by default into a single notification per microtask, with an opt-in synchronous path for controlled React inputs and other code that must observe each mutation immediately.
+The `createClassyStore()` function wraps a class instance in an ES6 Proxy. All mutations β property writes, array operations, nested object changes β are intercepted. Subscribers are batched by default into a single notification per microtask, while React always uses the synchronous path. Other code can opt into that path when it must observe each mutation immediately.
### Data Flow: Mutation β Notification
@@ -186,7 +186,7 @@ type StoreInternal = {
### Batching via `queueMicrotask`
-Array operations like `push()` trigger multiple SET traps (element + length). Rather than notifying per-trap, a dirty flag + `queueMicrotask` coalesces all synchronous mutations into one notification:
+Array operations like `push()` trigger multiple SET traps (element + length). For ordinary listeners, a dirty flag + `queueMicrotask` coalesces these mutations into one notification:
```
store.items.push('a')
@@ -195,7 +195,7 @@ store.items.push('a')
β microtask fires β notifies listeners ONCE
```
-Subscribers opt into immediate timing with `{sync: true}`. Sync subscribers run after version propagation and before the batched microtask. They are intentionally not deduped: two writes mean two sync notifications. React controlled inputs use this timing so `useSyncExternalStore` can observe the new external-store value during the input event turn, preserving caret position and IME composition.
+Subscribers opt into immediate timing with `{sync: true}`. Sync subscribers run after version propagation and before the batched microtask. They are intentionally not deduped: two writes mean two sync notifications. Every React hook uses this timing so `useSyncExternalStore` observes the new value during the input event, preserving controlled-input caret position. Selectors can run after each write; React batches rendering within event handlers.
### Version Propagation
@@ -343,7 +343,15 @@ flowchart TD
### Overview
-`useClassyStore` uses `useSyncExternalStore` for tear-free React integration. Subscriptions are batched by default, and can opt into synchronous notification with `{sync: true}` for controlled inputs that need React to observe the store update during the input event turn. It supports two modes:
+`useClassyStore` uses `useSyncExternalStore` for tear-free React integration. React subscriptions always notify synchronously, allowing controlled inputs to observe updates during their event. The React API has no timing option; ordinary core subscriptions remain batched. It supports two modes:
+
+### Server rendering and hydration
+
+`prepareStoreForHydration(store)` captures an immutable snapshot in a WeakMap keyed by the exact store proxy. It returns the same mutable proxy and preserves the first capture on repeated calls. Capturing is explicit at bootstrap, before rendering or persistence, because a delayed Suspense boundary may not call its hook until after the live store changes.
+
+Prepared stores supply that snapshot through `getServerSnapshot`; ordinary `getSnapshot` still reads live state. Each selector has separate server and live caches. When React reads the hydration result, it can seed the live cache with that result so unchanged inputs and equal selections retain their references without overwriting committed selections during rendering. Client-only mounts never evaluate the server selector.
+
+The capture stays available for the store's lifetime, including later boundaries or roots. It is never cleared when the first consumer mounts. Unprepared stores keep the existing current-snapshot behavior. Core mutation timing, ordinary subscriptions, and `snapshot(store)` are unchanged.
### Mode 1: Selector
@@ -373,17 +381,17 @@ sequenceDiagram
```
**Equality chain:**
-1. Same snapshot reference? β skip selector entirely (O(1))
+1. Same store, selector, equality function, and snapshot reference? β reuse the cached result (O(1))
2. Run selector β compare result with `Object.is` (or `options.isEqual`)
3. Same result β return previous reference (no re-render)
**Controlled input timing:**
```typescript
-const name = useClassyStore(formStore, (state) => state.name, {sync: true});
+const name = useClassyStore(formStore, (state) => state.name);
```
-The core store still batches subscribers by default with `queueMicrotask`. A controlled input subscription can opt into sync timing so the `useSyncExternalStore` callback runs in the same mutation turn as the input event, preserving caret position and IME composition.
+The hook always calls the core `subscribe()` with `{sync: true}`. Ordinary listeners still batch with `queueMicrotask`. This timing is independent of React's own render scheduling and does not provide atomic transactions across several writes.
### Mode 2: Auto-tracked (selectorless)
@@ -393,39 +401,18 @@ const snap = useClassyStore(myStore);
// snap.user.name β tracked
```
-```mermaid
-sequenceDiagram
- participant React as React (useSyncExternalStore)
- participant Hook as useClassyStore
- participant Snap as snapshot()
- participant PC as proxy-compare
+Each render creates its own `affected` map and raw-snapshot getter; the hook retains a proxy cache across renders. `useSyncExternalStore` receives a cached raw snapshot: unchanged tracked values return the previous snapshot for that render. After reading it, the hook wraps the snapshot with `createProxy` to record the component's property reads.
- React->>Hook: getSnapshot()
- Hook->>Snap: snapshot(proxy)
- Snap-->>Hook: frozen snapshot
+A parent re-render starts from current store data and a render-local `TrackingReadMap`. It lazily copies committed operation records for reused immutable snapshot objects, including structurally shared children. This retains reads skipped by React Compiler or manual memoization. Sets are copied before recording new reads, so suspended and abandoned renders cannot change committed dependencies. An effect publishes only committed reads into a WeakMap; old snapshot objects can be collected without retaining a chain of prior render maps.
- alt Same snapshot ref
- Hook-->>React: return cached tracking proxy
- else Different snapshot
- Hook->>PC: isChanged(prevSnap, nextSnap, affected)
- alt No tracked prop changed
- Hook-->>React: return cached tracking proxy (no re-render)
- else Tracked prop changed
- Hook->>PC: createProxy(nextSnap, affected)
- PC-->>Hook: new tracking proxy
- Hook-->>React: return new tracking proxy (re-render)
- end
- end
+The adapter follows the operation-record/Set layout of the pinned `proxy-compare` 3 dependency without hardcoding its private field names. Compiled regressions cover value reads, `in`, `Object.hasOwn`, and key enumeration. Reused snapshot objects conservatively retain prior committed dependencies, so changing a conditional branch may cause an extra render before that object's identity changes. Explicit selectors provide selected-value equality when stricter filtering is needed.
- Note over React: Component renders, reads snap.X
- React->>PC: snap.X (GET intercepted)
- PC->>PC: Record 'X' in affected WeakMap
-```
+Snapshot creation marks only library-created snapshot nodes with `markToTrack`, including class and null-prototype objects. This enables field tracking without altering opaque leaves such as native maps and dates. Nodes containing object-valued symbol fields use whole-node comparison to avoid a frozen-symbol limitation in `proxy-compare` 3; updates remain reactive with coarser render filtering.
**How `proxy-compare` works:**
-- `createProxy(snapshot, affected)` wraps the frozen snapshot in a Proxy that records which properties are read into the `affected` WeakMap
-- `isChanged(prevSnap, nextSnap, affected)` checks if any tracked property differs between two snapshots
-- Only structural changes to accessed properties cause re-renders
+- `createProxy(snapshot, affected)` records properties read into the render's map; committed records are retained separately with weak snapshot keys.
+- `isChanged(previousSnapshot, nextSnapshot, affected)` checks whether a tracked property differs.
+- Unchanged tracked properties suppress subscription-driven renders; `snapshot()` retains its existing identity cache and structural sharing.
## Dependency Graph
@@ -471,7 +458,7 @@ ES6 Proxy supports intercepting property creation, deletion, and arbitrary key a
### Why `queueMicrotask` for batching?
-`queueMicrotask` runs after the current synchronous code but before the next paint/timer. This means:
+`queueMicrotask` runs after the current synchronous code but before the next paint/timer. For ordinary subscribers, this means:
- Multiple mutations in the same event handler β 1 notification
- Array `push` (which triggers multiple SET traps) β 1 notification
- No risk of missing updates (unlike `requestAnimationFrame`)
@@ -517,7 +504,7 @@ No special handling is needed β three mechanisms in the existing design make i
|-----------|-----------|-------|
| Property read | O(1) | Direct Proxy GET trap |
| Property write | O(depth) | Versions bump from node to root |
-| Notification | O(listeners) | Once per microtask batch |
+| Notification | O(listeners) | Ordinary listeners once per microtask; React listeners per mutation |
| Getter (cache hit) | O(deps) | Check deps validity, return cached |
| Getter (cache miss) | O(getter body) | Re-evaluates with dep tracking |
| Snapshot (cache hit) | O(1) | Version check only |
diff --git a/website/docs/PERSIST_ARCHITECTURE.md b/website/docs/PERSIST_ARCHITECTURE.md
index 3d83c03..9c185d6 100644
--- a/website/docs/PERSIST_ARCHITECTURE.md
+++ b/website/docs/PERSIST_ARCHITECTURE.md
@@ -4,7 +4,7 @@ Internal design documentation for the `persist()` utility in `@codebelt/classy-s
## Design Principles
-1. **Standalone utility, not middleware.** `persist` is a plain function that consumes two existing public APIs (`subscribe` and `snapshot`) and nothing else. No changes to `core.ts`, `snapshot.ts`, or `useClassyStore` were needed.
+1. **Standalone utility, not middleware.** `persist` consumes the existing `subscribe`, `snapshot`, and `getVersion` APIs. No changes to core reactivity or React hooks are needed.
2. **Tree-shakeable.** `persist` lives in a separate entry point (`@codebelt/classy-store/utils`). Applications that don't use persistence pay zero cost -- the code is never bundled.
@@ -65,7 +65,10 @@ Each `persist()` call creates a closure with the following internal state:
| Variable | Type | Purpose |
|---|---|---|
| `disposed` | `boolean` | Guards against writes after `unsubscribe()` |
-| `debounceTimer` | `ReturnType \| null` | Active debounce timer (cancelled on save/unsubscribe) |
+| `debounceTimer` | `ReturnType \| null` | Active debounce timer (cancelled on save/clear/unsubscribe or accepted hydration) |
+| `storageQueue` | `Promise \| undefined` | Orders this handle's writes/removals and lets reads wait for prior operations |
+| `hydrationRequest` | `number` | Rejects read results superseded by newer requests or storage events |
+| `clearedVersion` | `number \| undefined` | Suppresses a pre-clear mutation's pending batched auto-save |
| `hydratedFlag` | `boolean` | Synchronous hydration status |
| `expiredFlag` | `boolean` | Set to `true` when hydration encounters expired data |
| `resolveHydrated` | `() => void` | Resolves the `hydrated` promise |
@@ -82,7 +85,7 @@ Triggered by `subscribe(proxy, callback)` firing after each batched mutation:
```
1. subscribe callback fires
-2. if disposed β return (guard)
+2. if disposed, applying hydration, or this version was cleared β return
3. if debounce > 0:
cancel existing timer
schedule new timer β goto step 4 after delay
@@ -96,9 +99,15 @@ Triggered by `subscribe(proxy, callback)` firing after each batched mutation:
6. wrap in envelope: { version, state }
7. if expireIn is set β stamp expiresAt = Date.now() + expireIn
8. JSON.stringify(envelope)
-9. await storage.setItem(name, json)
+9. enqueue storage.setItem(name, json) behind prior writes/removals
```
+The serialized value is captured when requested. The first queued operation
+starts immediately, so synchronous adapters keep their normal timing. Each
+operation catches adapter errors and reports them through `onError`; a rejected
+write does not prevent the next operation from starting. Cleanup skips operations
+that have not started. This queue belongs to one handle, not the storage key globally.
+
### Property Resolution
On initialization, `resolveProperties()` determines which keys to persist:
@@ -136,6 +145,13 @@ Triggered in three scenarios:
All three call the same `applyPersistedState(raw)` function:
+Async reads first wait for prior writes/removals. They capture a request number
+and the store version, and only apply their result if both are still current and
+the handle remains active. A newer read/save/clear/storage event changes the
+request number; a local edit changes the store version. Obsolete results and
+read errors are ignored. Applying hydration suppresses echo writes until the
+batched subscription notification has run, with cleanup in `finally`.
+
```
1. JSON.parse(raw) β envelope
2. Validate envelope shape (object with .state)
@@ -171,7 +187,7 @@ Both `'shallow'` and `'replace'` produce the same result for flat stores. The di
- **Corrupted JSON** in storage (`JSON.parse` throws): caught silently, hydration completes with no state changes.
- **Invalid envelope** (missing `.state` or not an object): detected by shape check, skipped silently.
-- **Async errors** during `storage.getItem()`: caught, hydrated promise rejects, `hydratedFlag` still set to `true`.
+- **Adapter read errors** during `storage.getItem()`: reported through `onError` when the request is current; hydration completes. Exceptions from custom migration/merge/transform callbacks can reject hydration.
## Cross-Tab Sync
@@ -189,20 +205,17 @@ sequenceDiagram
Event->>TabB: onStorageEvent fires
TabB->>TabB: if key !== name β ignore
TabB->>TabB: if newValue is null β ignore (cleared)
+ TabB->>TabB: if storageArea now contains another value β ignore (obsolete)
TabB->>TabB: applyPersistedState(newValue)
Note over TabB: proxy[key] = value β SET trap β reactivity
```
Key properties of `window.storage`:
-- Fires in **other** same-origin tabs only (not the tab that wrote). This prevents infinite loops.
+- For `localStorage`, fires in **other** same-origin tabs (not the tab that wrote). The hydration guard prevents echo writes and loops.
- Only works with `localStorage` -- not `sessionStorage`, not async adapters.
- The event payload includes `key`, `oldValue`, `newValue`, and `storageArea`.
-### Auto-detection
-
-If `syncTabs` is not explicitly set:
-- `true` when the storage adapter is literally `globalThis.localStorage` (checked via identity comparison).
-- `false` for everything else (`sessionStorage`, `AsyncStorage`, custom adapters).
+`syncTabs` defaults to `false`; enable it explicitly for localStorage-backed state.
## Cleanup
@@ -218,19 +231,20 @@ If `syncTabs` is not explicitly set:
After `unsubscribe()`:
- Store mutations no longer trigger writes to storage.
- Cross-tab changes to the storage key are ignored.
-- `save()` becomes a no-op.
-- `clear()` and `rehydrate()` still work (they operate on storage directly, not on subscriptions).
+- `save()`, `clear()`, and `rehydrate()` become no-ops.
+- Queued writes/removals that have not started are skipped. Already-issued adapter I/O can still finish.
+- Pending read results/errors are ignored, and their promises settle when the reads finish.
## PersistHandle API
| Method/Property | Behavior |
|---|---|
| `unsubscribe()` | Full cleanup: unsubscribe + cancel debounce + remove storage listener. Idempotent. |
-| `hydrated` | Promise that resolves when initial hydration completes. Rejects on async storage errors. |
+| `hydrated` | Promise that resolves when initial hydration completes or is superseded. Custom hydration callback exceptions can reject it. |
| `isHydrated` | Getter returning `boolean`. Becomes `true` after hydration resolves or rejects. |
| `isExpired` | Getter returning `boolean`. `true` when the last hydration found expired data (requires `expireIn`). Re-evaluated on every `rehydrate()` call. |
-| `save()` | Cancel pending debounce, write current state immediately. No-op if disposed. |
-| `clear()` | Call `storage.removeItem(name)`. Works even after unsubscribe. |
+| `save()` | Cancel pending debounce, capture current state, and enqueue its write. No-op if disposed. |
+| `clear()` | Cancel pending auto-saves and enqueue `storage.removeItem(name)` after earlier writes. No-op if disposed. |
| `rehydrate()` | Re-read from storage, apply to store. Also resolves `hydrated` if not yet resolved (for `skipHydration` flows). |
## Dependency Graph
diff --git a/website/docs/PERSIST_TUTORIAL.md b/website/docs/PERSIST_TUTORIAL.md
index 8f89ed5..4f7802b 100644
--- a/website/docs/PERSIST_TUTORIAL.md
+++ b/website/docs/PERSIST_TUTORIAL.md
@@ -161,7 +161,7 @@ On restore, `deserialize` converts the ISO string back into a `Date` object befo
### `storage`
-`persist()` handles different storage adapters transparently. The default is `localStorage`. Any object with `getItem`, `setItem`, and `removeItem` (sync or async) works.
+`persist()` requires a storage adapter. Any object with `getItem`, `setItem`, and `removeItem` (sync or async) works, including `localStorage`.
**sessionStorage (Tab-Scoped)**
@@ -225,6 +225,31 @@ persist(uiStore, {
Cross-tab sync only works with `localStorage`. It does not fire for `sessionStorage`, `AsyncStorage`, or custom adapters.
+An event whose value has already been replaced in its storage area is ignored.
+This prevents a delayed event from rolling a newer local save back in the UI.
+Accepted remote updates cancel pending debounced auto-saves and do not write
+the restored state back to storage.
+
+### Overlapping async operations
+
+Each persistence handle orders its writes and removals. A slow earlier save
+cannot finish after a later save and overwrite it. `save()` captures the current
+state when requested and resolves after that write completes; a failed write
+reports through `onError` and does not block later writes.
+
+`rehydrate()` waits for writes/removals already requested through that handle.
+If another read, save, clear, accepted storage event, or local mutation supersedes
+an in-flight read, its result is ignored. In particular, typing while initial
+async hydration is pending keeps the newer local state. An ignored read still
+settles its promise; hydration completion does not guarantee that stored data won.
+
+`clear()` cancels pending auto-saves and removes stored data after earlier issued
+writes finish. Later edits can persist again. `unsubscribe()` skips queued writes
+that have not started; it cannot cancel adapter I/O already in progress.
+
+Ordering is per handle. Separate handles sharing an async storage key still
+need coordination at the application or adapter level.
+
### `expireIn` & `clearOnExpire` (Expiration / TTL)
Use `expireIn` to set a time-to-live (in milliseconds) on persisted data. After the TTL elapses, stored data is skipped during hydration and the store keeps its class defaults.
@@ -514,7 +539,10 @@ window.addEventListener('beforeunload', () => {
});
```
-`save()` cancels the pending debounce timer and writes the current state immediately.
+`save()` cancels the pending debounce timer and requests a write of the current
+state immediately. With async storage it waits behind earlier writes. Await the
+returned promise when completion matters; an async write is not guaranteed to
+finish during page unload.
### `unsubscribe()` (Stop persisting)
@@ -530,6 +558,8 @@ handle.unsubscribe();
todoStore.addTodo('This is not persisted');
```
+Pending storage reads that finish after unsubscribe no longer apply state, clear expired entries, or invoke read-error callbacks. Hydration promises still settle when the read finishes. This prevents a retired React effect from overwriting a newer scope during Strict Mode replay, resets, or storage configuration changes. Already-issued adapter I/O is not cancelled; cleanup cannot undo a write already sent to storage. See the [typed context lifecycle example](./REACT_SCOPING.md).
+
---
## Multiple Persistence Targets
diff --git a/website/docs/REACT_NEXT.md b/website/docs/REACT_NEXT.md
new file mode 100644
index 0000000..5781f87
--- /dev/null
+++ b/website/docs/REACT_NEXT.md
@@ -0,0 +1,141 @@
+# Next.js App Router
+
+Pass initial data from a Server Component to a Client Component, then construct
+the class store inside the client boundary. That component also renders on the
+server for the initial request, so prepare its snapshot before persistence or
+other effects start. The browser constructs its own store from the same data.
+
+Do not pass a store proxy or class instance across the React Server Component
+boundary. Use a plain data object containing the fields the client needs.
+React defines the supported [serializable prop types](https://react.dev/reference/rsc/use-client#serializable-types-returned-by-server-components).
+
+## Package boundaries
+
+| Entry | App Router use |
+| --- | --- |
+| `@codebelt/classy-store` | Core store creation and snapshots in server or client modules. |
+| `@codebelt/classy-store/collections` | Reactive collections in server or client modules. |
+| `@codebelt/classy-store/utils` | Server-safe imports; configure browser storage and effects inside Client Components. |
+| `@codebelt/classy-store/react` | Client hooks and the compatible `prepareStoreForHydration` export. The published ESM and CommonJS entries carry `use client`. |
+| `@codebelt/classy-store/react/server` | `prepareStoreForHydration` without importing React hooks; safe in Server Components and ordinary SSR code. |
+
+Components that use hooks still need their own client boundary. Do not call
+`useClassyStore`, `useLocalStore`, or `createStoreHook` in a Server Component.
+Preparing a store in an RSC module does not transfer that store or its capture
+to the browser; initialize and prepare the browser counterpart separately.
+Both preparation exports use the same capture cache within a module graph.
+
+## A route-owned store
+
+Define the class in a regular module that has no browser effects:
+
+```ts
+// stores/profile/profile.store.ts
+export type ProfileData = {name: string; visits: number};
+
+export class ProfileStore {
+ name: string;
+ visits: number;
+
+ constructor(initialData: ProfileData) {
+ this.name = initialData.name;
+ this.visits = initialData.visits;
+ }
+
+ recordVisit() {
+ this.visits++;
+ }
+}
+```
+
+Construct and prepare it inside the Client Component:
+
+```tsx
+// components/profileEditor/ProfileEditor.tsx
+'use client';
+
+import {
+ prepareStoreForHydration,
+ useClassyStore,
+ useLocalStore,
+} from '@codebelt/classy-store/react';
+import {type ProfileData, ProfileStore} from '../../stores/profile/profile.store';
+
+export function ProfileEditor({initialData}: {initialData: ProfileData}) {
+ const store = prepareStoreForHydration(
+ useLocalStore(() => new ProfileStore(initialData)),
+ );
+ const state = useClassyStore(store);
+
+ return (
+
+ { store.name = event.target.value; }}
+ />
+ store.recordVisit()}>
+ Visits: {state.visits}
+
+
+ );
+}
+```
+
+The Server Component supplies data and an intentional reset key:
+
+```tsx
+// app/profile/page.tsx
+import {ProfileEditor} from '../../components/profileEditor/ProfileEditor';
+
+type Props = {searchParams: Promise<{name?: string}>};
+
+export default async function ProfilePage({searchParams}: Props) {
+ const {name = 'Ada'} = await searchParams;
+ return ;
+}
+```
+
+The key determines when a mounted editor gets a new store. A new `initialData`
+object alone does not reset `useLocalStore` or its prepared snapshot. Avoid
+module-level server stores; construct request state inside the request instead.
+
+## Streaming, persistence, and navigation
+
+Attach persistence in an effect after preparation, call `rehydrate()` there,
+and return `unsubscribe()` from cleanup. The [SSR guide](./REACT_SSR.md) shows
+this pattern. If persistence restores before a streamed boundary hydrates,
+the boundary first reads the prepared server data, then catches up to live
+state. Keep the initial server and browser payloads identical.
+
+For a store shared across routes, place its Client Provider in the appropriate
+layout. Put route-specific providers below that layout and use keys when a
+route identity change should reset them. See the [typed context example](./REACT_SCOPING.md).
+`router.refresh()` merges new server output; it does not inherently reset a
+mounted client store. Choose explicitly whether refreshed data should update
+the existing store or replace its provider via a key.
+
+The integration tests use the default App Router configuration with
+`cacheComponents: false`. With Cache Components enabled, Next.js can use React
+Activity to preserve hidden route state while cleaning up its effects. Returning
+to a hidden route can resume the same store; effect cleanup does not necessarily
+mean the instance was destroyed. See [Next.js state preservation](https://nextjs.org/docs/app/guides/preserving-ui-state).
+
+## Tested package consumer
+
+Run `bun run test:react:next` from the repository root. It installs the package
+tarball into an isolated Next.js 16.4.2 / React 19.3.1 app and runs clean
+production Turbopack builds with React Compiler off and on. Tests verify the
+actual compiler-generated selector and tracking functions, request isolation,
+real inputs, streamed response ordering, delayed hydration without replacing
+server DOM, navigation, refresh, and persistence cleanup. They also exercise
+the server-safe APIs under React's `react-server` export condition and verify
+shared hydration captures across public entry points.
+
+This matrix covers the stated versions and default navigation behavior. It
+does not establish compatibility with every Next.js version, bundler, or
+Cache Components configuration.
+
+Next App Router can supply its own React runtime. The test evidence records the
+actual browser `React.version`; this Next.js version uses
+`19.3.0-canary-278794d7-20261002`, while the installed React package is 19.3.1.
diff --git a/website/docs/REACT_SCOPING.md b/website/docs/REACT_SCOPING.md
new file mode 100644
index 0000000..d690c35
--- /dev/null
+++ b/website/docs/REACT_SCOPING.md
@@ -0,0 +1,152 @@
+# React store scoping and lifecycle
+
+Use React context to choose a store instance for a subtree, then `useClassyStore` to subscribe to that instance. This is useful for independent forms, editors, nested workspaces, and request-scoped server rendering.
+
+The runnable [scoped editors demo](https://codebelt.github.io/classy-store/demos/#/scoped-stores) shows two independent editors and a nested editor. Its [source](https://github.com/codeBelt/classy-store/tree/main/examples/react/src/demos/scoped-store) is an application example built from existing APIs; there is no new context factory to install.
+
+## Define a typed context
+
+Share the context definition across the application. Create store instances inside each provider, so server requests and sibling providers cannot accidentally share mutable state.
+
+```ts
+// editorStore.ts
+export type EditorData = {title: string; body: string};
+
+export class EditorStore {
+ title: string;
+ body: string;
+
+ constructor(initialData: EditorData) {
+ this.title = initialData.title;
+ this.body = initialData.body;
+ }
+
+ setTitle(title: string) { this.title = title; }
+ setBody(body: string) { this.body = body; }
+}
+```
+
+```ts
+// editorStoreContext.ts
+import {createContext} from 'react';
+import type {EditorStore} from './editorStore';
+
+export const EditorStoreContext = createContext(null);
+```
+
+```ts
+// hooks/useEditorStore.ts
+import {useContext} from 'react';
+import {EditorStoreContext} from '../editorStoreContext';
+
+export function useEditorStore() {
+ const store = useContext(EditorStoreContext);
+ if (!store) {
+ throw new Error('useEditorStore must be used within EditorStoreProvider');
+ }
+ return store;
+}
+```
+
+The hook returns a typed mutable `EditorStore`, with its methods intact. It fails clearly when a provider is missing. React resolves the [closest matching provider](https://react.dev/reference/react/useContext), so a nested provider overrides the instance only for its own subtree.
+
+## Let the provider own creation and effects
+
+The example provider initializes once per mount and captures the initial hydration data before starting persistence:
+
+```tsx
+// components/editorStoreProvider/EditorStoreProvider.tsx
+import {prepareStoreForHydration, useLocalStore} from '@codebelt/classy-store/react';
+import {persist, type StorageAdapter} from '@codebelt/classy-store/utils';
+import {type ReactNode, useEffect} from 'react';
+import {type EditorData, EditorStore} from '../../editorStore';
+import {EditorStoreContext} from '../../editorStoreContext';
+
+type Props = {
+ initialData: EditorData;
+ children: ReactNode;
+ persistence?: {name: string; storage: StorageAdapter};
+};
+
+export function EditorStoreProvider({initialData, children, persistence}: Props) {
+ const store = prepareStoreForHydration(
+ useLocalStore(() => new EditorStore(initialData)),
+ );
+ const name = persistence?.name;
+ const storage = persistence?.storage;
+
+ useEffect(() => {
+ if (name === undefined || storage === undefined) return;
+ const handle = persist(store, {
+ name,
+ storage,
+ properties: ['title', 'body'],
+ skipHydration: true,
+ });
+ void handle.rehydrate();
+ return () => handle.unsubscribe();
+ }, [store, name, storage]);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+Keep constructors and initializers pure: Strict Mode can evaluate them more than once. Start subscriptions, timers, and storage work in effects with cleanup. The persistence effect depends on the storage adapter and name, so passing a new options object with those same values does not restart it. Keep a custom storage adapter's identity stable too.
+
+The context holds the stable store proxy. Consumers read snapshots through the existing React hook and send edits through store methods:
+
+```tsx
+// components/editorTitle/EditorTitle.tsx
+import {useClassyStore} from '@codebelt/classy-store/react';
+import {useEditorStore} from '../../hooks/useEditorStore';
+
+export function EditorTitle() {
+ const store = useEditorStore();
+ const title = useClassyStore(store, (state) => state.title);
+ return store.setTitle(event.target.value)} />;
+}
+```
+
+Auto-tracking also works: `const state = useClassyStore(useEditorStore())`. Reading fields directly from the mutable store during render does not subscribe to mutations. Avoid creating a module-level bound hook for one store if consumers must select different instances through context.
+
+## Preserve drafts and reset intentionally
+
+`initialData` is an initializer. New props or an ordinary parent render leave the existing store and draft intact. Use a [changed React key](https://react.dev/learn/preserving-and-resetting-state#resetting-state-with-a-key) to create a fresh session:
+
+```tsx
+
+
+
+```
+
+Change `documentId` when moving to a different document; increment `revision` for an explicit reset. A key change remounts the provider's whole subtree, including nested providers. Resetting a nested provider leaves its parent and siblings intact.
+
+| Event | Store and effect behavior |
+| --- | --- |
+| Parent render or changed `initialData`, same key | Preserve the existing store and edits |
+| Provider key changes | Create a new store and clean up the old subtree |
+| Nested provider | Consumers inside it use its independent store |
+| Storage name or adapter changes | Keep the store; detach old persistence and load from the new configuration |
+| Persistence removed | Keep current state and stop persistence |
+| Provider unmounts | React releases its store reference and effect cleanup detaches persistence |
+
+A reset does **not** clear saved data. If the new provider uses the same persistence key, it restores that saved draft. To reset saved data as well, first unmount or disable the old persistence owner, await `storage.removeItem(name)`, then remount. Use distinct storage keys for independent documents or users. Different React providers using the same storage key still share that storage namespace.
+
+Cleanup prevents pending storage reads from applying after unsubscribe, including the discarded first effect in Strict Mode. Such reads still finish and their promises settle; the adapter's I/O is not cancelled. Already-issued writes cannot be undone by cleanup. External code retaining a store can still mutate it after unmount, but its disposed persistence owner and unmounted consumers no longer react. `useLocalStore` does not automatically call application-specific `dispose()` methods.
+
+## Server rendering and hydration
+
+Render a fresh provider for each server request. Transfer only its initial data through the framework's serialization facility, then initialize the client provider from that same data. The provider prepares the initial snapshot before any persistence effect, so a delayed Suspense boundary can hydrate correctly even after another part of the tree restores browser storage. See the [SSR guide](./REACT_SSR.md) for transport and streaming details.
+
+Browser storage belongs in the client setup; do not access `localStorage` at module initialization on the server. Effects do not run during server rendering. The server and client's initial markup must match regardless of whether browser persistence is enabled.
+
+## Regression coverage
+
+`bun run test:react:compatibility` copies the actual example provider, context, hook, and class into isolated installed-package consumers on React 18.0, 18.3, and 19.3. Tests cover sibling and nested isolation, missing providers, stable initialization, keyed resets, Strict Mode, persistence replacement/removal, pending-read cleanup, request isolation, and delayed Suspense hydration with DOM reuse. Strict type checks verify mutable commands, readonly snapshots, selector inference, and provider props.
diff --git a/website/docs/REACT_SSR.md b/website/docs/REACT_SSR.md
new file mode 100644
index 0000000..711a1a2
--- /dev/null
+++ b/website/docs/REACT_SSR.md
@@ -0,0 +1,108 @@
+# React server rendering and hydration
+
+Create one store for each server request and reconstruct a client store from the same initial data. Pass the store through props or React context. A module-level store on the server shares mutable state across requests and can expose one user's data to another.
+
+The [typed context example](./REACT_SCOPING.md) provides a complete provider pattern with independent instances, nested scopes, keyed resets, and persistence cleanup.
+
+For React Server Components, use the server-safe `@codebelt/classy-store/react/server` preparation entry. The `@codebelt/classy-store/react` hooks entry is marked `use client`; its preparation export remains available inside client modules and ordinary SSR. The [Next.js App Router guide](./REACT_NEXT.md) explains where to construct stores and how to cross that boundary with initial data.
+
+Call `prepareStoreForHydration(store)` after initializing the store with the data used to render the page, on both the server and client. It captures that initial snapshot before persistence or other live updates begin. All direct and bound hooks use the captured data during server rendering and client hydration, then read the current live store. React requires the initial server and client data to match; see [React's external-store SSR guidance](https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering).
+
+## Initialize from request data
+
+Keep the serialized payload limited to the data the client needs. Reconstruct the class so its methods and getters remain available:
+
+```ts
+// profileStore.ts
+import {createClassyStore} from '@codebelt/classy-store';
+import {prepareStoreForHydration} from '@codebelt/classy-store/react/server';
+
+export type ProfileData = {
+ name: string;
+ visits: number;
+};
+
+export class ProfileStore {
+ name: string;
+ visits: number;
+
+ constructor(initialData: ProfileData) {
+ this.name = initialData.name;
+ this.visits = initialData.visits;
+ }
+
+ recordVisit() {
+ this.visits++;
+ }
+}
+
+export function createProfileStore(initialData: ProfileData) {
+ return prepareStoreForHydration(
+ createClassyStore(new ProfileStore(initialData)),
+ );
+}
+```
+
+```tsx
+// Profile.tsx
+import {useClassyStore} from '@codebelt/classy-store/react';
+import type {ProfileStore} from './profileStore';
+
+export function Profile({store}: {store: ProfileStore}) {
+ const state = useClassyStore(store);
+ return (
+ store.recordVisit()}>
+ {state.name}: {state.visits}
+
+ );
+}
+```
+
+For each request, render ` `. Transfer `initialData` using the framework's serialization facility and initialize the browser store from that same payload before hydration. If implementing the HTML transport yourself, use safe JSON serialization for the embedding context; do not interpolate raw JSON into an executable script. Class instances, functions, and native collection types need an explicit data representation.
+
+For a store owned by a component, `prepareStoreForHydration(useLocalStore(() => new ProfileStore(initialData)))` captures its initial state while keeping its identity stable across renders. Preparation is idempotent. Keep the factory pure: React Strict Mode can evaluate it more than once in development. Changes to `initialData` after mounting do not automatically reset the store or its captured snapshot.
+
+## Restore persistence while other boundaries hydrate
+
+Browser storage may contain values different from the server-rendered payload. Prepare the store before starting persistence, attach persistence in an effect, and unsubscribe when the component unmounts. The effect can restore storage even while a Suspense boundary is still waiting to hydrate:
+
+```tsx
+// PersistedProfile.tsx
+import {useEffect} from 'react';
+import {prepareStoreForHydration, useLocalStore} from '@codebelt/classy-store/react';
+import {persist} from '@codebelt/classy-store/utils';
+import {Profile} from './Profile';
+import {type ProfileData, ProfileStore} from './profileStore';
+
+export function PersistedProfile({initialData}: {initialData: ProfileData}) {
+ const store = prepareStoreForHydration(
+ useLocalStore(() => new ProfileStore(initialData)),
+ );
+
+ useEffect(() => {
+ const persistence = persist(store, {
+ name: 'profile',
+ storage: localStorage,
+ skipHydration: true,
+ });
+ void persistence.rehydrate();
+ return () => persistence.unsubscribe();
+ }, [store]);
+
+ return ;
+}
+```
+
+For example, the server renders `Server:1`, persistence restores `Saved:8`, and a delayed boundary then hydrates. The boundary first reads `Server:1` from the captured snapshot, reuses the server DOM, and then updates to `Saved:8` from the live store. It does not need an earlier hook read to establish the initial data. This applies to completed server HTML with selective hydration and to streamed Suspense content.
+
+## Snapshot lifetime and scope
+
+- Capture after loading the initial request data, before rendering or starting persistence. Preparation cannot recover earlier data if the store has already changed.
+- Repeated calls return the same mutable proxy and retain the first snapshot. The capture is kept for the store's lifetime because another boundary or root may hydrate later. Use a fresh store for a different request or initial payload.
+- Prepare the exact proxy passed to `useClassyStore` or `createStoreHook`. If a hook subscribes directly to a nested proxy, prepare that proxy separately.
+- Capture follows ordinary `snapshot()` semantics. Plain objects, arrays, and supported class state are immutable snapshots; opaque leaves such as native `Date`, `Map`, and `Set` retain their identity. Replace those fields instead of mutating their native objects in place.
+- Selectors read captured data during hydration and live data afterward. Their caches are separate, while equal selected results preserve reference identity at the handoff.
+
+Client-only apps need no preparation. `createRoot` reads current state even for a prepared store. Unprepared stores retain the existing behavior of reading current state for server rendering and hydration, so keep them unchanged until every consumer hydrates if you do not use preparation. Neither preparation nor hydration resets the live store; `snapshot(store)` continues to return current data.
+
+React subscriptions notify synchronously by default, including after hydration. Ordinary `subscribe(store, listener)` callbacks remain batched. The React hooks do not have a `sync` option. A normal sequence of mutations in an event can be batched into one React commit, but selectors may run after each mutation; this is not a transaction API.
diff --git a/website/docs/TUTORIAL.md b/website/docs/TUTORIAL.md
index 2740d70..bfeb38e 100644
--- a/website/docs/TUTORIAL.md
+++ b/website/docs/TUTORIAL.md
@@ -1,6 +1,6 @@
# Classy Store Tutorial
-`Classy Store` is a reactive state library for React, Vue, Svelte, Solid, and Angular built on ES proxies. You define state as plain classes, wrap them with `createClassyStore()`, and read them with the binding for your framework. There are no Providers, no observers, no reducers, and no extra TypeScript interfaces to define your state shape β just classes and a hook. The library is ~3.5 KB gzipped, batches synchronous mutations into a single re-render, and uses structural sharing for efficient change detection.
+`Classy Store` is a reactive state library for React, Vue, Svelte, Solid, and Angular built on ES proxies. You define state as plain classes, wrap them with `createClassyStore()`, and read them with the binding for your framework. There are no Providers, no observers, no reducers, and no extra TypeScript interfaces to define your state shape β just classes and a hook. The library is ~3.5 KB gzipped, batches ordinary subscriptions while notifying React synchronously, and uses structural sharing for efficient change detection.
## Quick Start
@@ -162,13 +162,9 @@ const remaining = useClassyStore(todoStore, (state) => state.remaining);
- **Use auto-tracked mode** when you'd need 3+ selectors in one component or when you're exploring the API.
- **Add `shallowEqual`** when your selector returns objects/arrays and you're seeing unnecessary re-renders.
-### Controlled inputs with `sync`
+### Controlled inputs
-Classy Store batches subscriber notifications by default. That is the right default for most UI because multiple synchronous mutations collapse into one settled update.
-
-Controlled React inputs are the important exception. React needs the latest external-store value during the input event turn so it can preserve the DOM value, caret position, and IME composition state. If the store waits until the next microtask to notify React, typing in the middle of a controlled input can move the caret or interrupt composition for languages such as Korean, Japanese, and Chinese.
-
-Use `{sync: true}` for the specific store reads that control form fields:
+React subscriptions always notify synchronously. React sees a field's updated value during the input event, so typing in the middle of a controlled input preserves its caret. Selector, auto-tracked, and bound hooks all use the same timing without configuration:
```tsx
class ProfileFormStore {
@@ -182,9 +178,7 @@ class ProfileFormStore {
const profileFormStore = createClassyStore(new ProfileFormStore());
function NameInput() {
- const name = useClassyStore(profileFormStore, (state) => state.name, {
- sync: true,
- });
+ const name = useClassyStore(profileFormStore, (state) => state.name);
return (
`, `