Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0150df8
feat: add experimental core package
doasync Jan 15, 2026
52afcbc
feat: add models research playground
doasync Jan 15, 2026
5b05698
feat(core-experimental): implement experimental API and fixes
doasync Jan 15, 2026
e47fd78
feat(models-research): implement game and user demos
doasync Jan 15, 2026
6cdf317
feat(core-experimental): stabilize prototype architecture
doasync Jan 15, 2026
3b828af
feat(models-research): update demos to use stable core API
doasync Jan 15, 2026
019ac1a
docs: update presentation and fixes plan
doasync Jan 15, 2026
7844b4c
feat: setup tailwind css v4 and responsive layout
doasync Jan 15, 2026
2fd064c
fix: improve user demo logic, add names and refine form
doasync Jan 15, 2026
199e04c
docs: add testing strategy and fix lens property access
doasync Jan 15, 2026
fd19b17
Fix Core Primitives
doasync Jan 15, 2026
7a1f7ca
Fix Core Primitives
doasync Jan 15, 2026
792d187
tests
doasync Jan 15, 2026
31de12c
test(core-experimental): implement comprehensive test suite with 100%…
doasync Jan 15, 2026
af043b1
refactor(core-experimental): flatten keyval state and fix variant logic
doasync Jan 15, 2026
d9a4fd6
chore: update research app
doasync Jan 15, 2026
a567beb
feat(experimental): implement effector models recursion, tags, and tr…
doasync Jan 15, 2026
dfafd0c
test(demo): achieve 100% coverage for recursive tree demo
doasync Jan 15, 2026
25e26a8
feat: enhance user demo with role-based logic and visuals
doasync Jan 15, 2026
66ec90f
fix: resolve build errors in models-research
doasync Jan 15, 2026
72078a9
chore: cleanup
doasync Jan 16, 2026
2406e77
feat(food): requirements
doasync Jan 16, 2026
2d2e014
feat(food): raw implementation
doasync Jan 16, 2026
96f9dfb
feat(food): add data
doasync Jan 16, 2026
157636d
refactor(food)
doasync Jan 17, 2026
12b5b1f
fix(food): full implementation
doasync Jan 17, 2026
3dd7002
feat(food): add KFC shop
doasync Jan 17, 2026
66a9710
fix(food): fix UI issues
doasync Jan 17, 2026
501d1bf
feat(food): move food to adds/fast-food
doasync Jan 18, 2026
7671561
chore(food): remove food from models-research
doasync Jan 18, 2026
3d5b545
feat(fast-food): double down on models
doasync Jan 18, 2026
9c3f5f7
feat(fast): add more type and tests
doasync Jan 18, 2026
6ab164d
feat(fast): create predictable system
doasync Jan 18, 2026
238ab72
fix(all): health check
doasync Jan 18, 2026
baf700d
chore: add changeset
doasync Jan 18, 2026
27f725c
ci: perform final validation and fix health check issues
doasync Jan 18, 2026
df08b0c
ci: add changeset
doasync Jan 18, 2026
7571ba6
chore: fix CI
doasync Jan 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ node_modules
*.log
.nx
.vscode
packages/core-experimental/coverage/*
apps/models-research/src/tree/__tests__/__screenshots__/*
packages/react/src/__tests__/examples/__screenshots__/*
51 changes: 51 additions & 0 deletions DEMO_IMPL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Implementation Plan: Recursive File System Demo

## 1. Data Model (`apps/models-research/src/tree/model.ts`)

### 1.1. Facets (`facets.ts`)

- **`nodeFacet`**:
- `$name`: `Store<string>`
- `$isSelected`: `Store<boolean>`
- `select`: `Event<void>`
- `rename`: `Event<string>`
- **`folderFacet`**:
- `$isOpen`: `Store<boolean>`
- `toggle`: `Event<void>`
- `children`: `Array<FileModel | FolderModel>` (Recursive)
- **`visualFacet`**:
- `$backgroundColor`: `Store<string>`
- `_selectionSource`: `ref.tag('$isSelected')`

### 1.2. Models

- **Selection Logic**: Use `combine($selectedId, id, ...)` to ensure reactive selection state that works on initial load.
- **Rename Logic**: `rename` event samples the new value directly into the `name` input store.
- **Recursion**: `folderModel` children defined via `define.array(ref.self)`.

## 2. Global State (`apps/models-research/src/app/TreeDemo.tsx`)

- **`$selectedId`**: Initialized to `'root-node'` to ensure a default selection.
- **ID Generation**: Unique IDs (`node-1`, `node-2`, etc.) generated during manual instantiation to facilitate single-selection logic.

## 3. View Layer (`apps/models-research/src/tree/view.tsx`)

- **Separation of Concerns**:
- `RecursiveTreeView`: Dispatches between File and Folder.
- `FileView` / `FolderView`: Handle double-click for editing and selection logic.
- **Interaction**:
- `toggle()` moved to the arrow icon (`<span>`) to allow selection of folders without collapsing them.
- Local React state (`isEditing`, `editName`) used for the renaming workflow to ensure stability of the global name store until commit.

## 4. Details & Breadcrumbs (`apps/models-research/src/app/TreeDemo.tsx`)

- **Breadcrumbs**:
- `getTrace(root, id)`: DFS traversal returning an array of model instances from root to target.
- `BreadcrumbItem`: A component per trace node that uses `useUnit(node.facets.node.$name)` to achieve full path reactivity on rename.
- **Details**:
- `NodeDetails`: Encapsulates hooks for the selected node, preventing React hook order violations when selection changes or is missing.

## 5. Core API Enhancements

- Updated `packages/core-experimental/src/instance.ts` to support `array` type facets, allowing recursive children lists to be used directly with `useUnit`.
- Ensured `ref.tag` resolution handles nested logic objects within model `fn` results.
53 changes: 53 additions & 0 deletions DEMO_RD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Requirements Document: Recursive File System Demo

## 1. Overview

This demo aims to validate and showcase advanced features of the Effector Models API: **Recursion** (`ref.self`) and **Internal Reference Resolution** (`ref.tag`). We built a functional **File Explorer** UI to demonstrate these concepts in a real-world scenario.

## 2. Core Concepts Demonstrated

### 2.1. Recursion (`ref.self`)

- **Requirement**: Support infinite nesting of content.
- **Implementation**: `FolderModel` defines its children as an array of model definitions using `ref.self`.
- **Verification**: The UI renders a nested structure correctly, with each node maintaining its own state (e.g., expansion state).

### 2.2. Internal References (`ref.tag`)

- **Requirement**: Declarative data sharing between decoupled facets within a single model instance.
- **Implementation**: `visualFacet` declares a dependency on `$isSelected` via `ref.tag('$isSelected')`.
- **Verification**: Background colors update reactively based on selection state without manual wiring in the factory function.

## 3. Functional Requirements

### 3.1. File Entities

- **File**: Represents a leaf node with a `name`.
- **Folder**: Represents a container node with a `name` and `children`.

### 3.2. User Interaction

- **Selection**:
- One node is **always** selected (defaults to the root node).
- Clicking any node moves the selection to that node.
- Selection cannot be "untoggled" by clicking the same node; it only moves to a different one.
- The selected node is highlighted with a light blue background.
- **Expansion**:
- Folders can be expanded or collapsed.
- **Crucial**: Toggling expansion only occurs when clicking the arrow emoji (▶/▼). Clicking the folder name only selects it.
- **Breadcrumbs**:
- A reactive path (e.g., `project-root > src > app.tsx`) is displayed above the tree.
- The path updates immediately if any node in the selection trace is renamed.
- **Details View**:
- Displays the name and type (File/Folder) of the selected node below the tree.
- **Renaming**:
- Double-clicking a node name enters "Edit Mode".
- Double-clicking also selects the node.
- Submitting (Enter) or clicking outside (Blur) saves the new name.
- The name in the details view remains stable (shows the old name) until the edit is committed.

## 4. Technical Implementation

- Used `combine` for reactive selection state to ensure initial visibility of the default selection.
- Implemented recursive trace calculation (`getTrace`) to provide reactive breadcrumbs via a chain of `useUnit` calls.
- Handled React hook lifecycle by encapsulating node-specific hooks in a separate `NodeDetails` component to prevent "Rendered more hooks" errors.
79 changes: 79 additions & 0 deletions FIXES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Fixes & Refinements Plan

This document outlines the critical fixes and architectural refinements needed for the `core-experimental` package to transition from a prototype to a stable implementation.

## ✅ Completed Fixes

### 1. `keyval` Trigger Conflicts (Double Execution)

**Severity**: High
**Location**: `packages/core-experimental/src/keyval.ts`
**Issue**:
When `getItem(event)` is used, two conflicting `sample`s are often created:

1. **Auto-wiring**: Inside `createItemProxy`, a `sample` is automatically created connecting the source `event` (ID) to the facet method's effect (`fx`), passing `undefined` as payload.
2. **Manual wiring**: The user often manually samples the source event to the returned unit (`sample({ clock: event, target: item.method })`).

This results in the method being executed **twice**: once with `undefined` payload (auto) and once with the correct ID (manual).

**Proposed Fix**:

- **Remove Auto-wiring**: `createItemProxy` should **not** automatically `sample` the source event to `fx` upon property access.
- **Explicit Wiring**: The returned unit (from property access) should be a "detached" event that, when triggered, executes the effect.
- **Refactor `match()`**: Since `match` currently relies on this auto-wiring side-effect (by just accessing the property), it must be updated to explicitly call or sample the method.

### 2. Action Routing Payload Support

**Severity**: High
**Location**: `packages/core-experimental/src/keyval.ts`
**Issue**:
Currently, `getItem(event)` assumes the event payload is _just_ the ID string (`Event<string>`). This makes it impossible to route actions that require data (e.g., `updateName({ id, newName })`).

**Proposed Fix**:

- **Support Complex Payloads**: Update `getItem` to accept `Event<{ id: string } & P>`.
- **Payload Extraction**: In `createItemProxy`, extract the payload (excluding `id`) and pass it to the facet method's effect.
- **Type Inference**: Improve TS types to infer the payload type from the event.

### 3. `select()` Reactivity (The `getState()` Hack)

**Severity**: Medium
**Location**: `packages/core-experimental/src/lens.ts`
**Issue**:
The current implementation of `select()` uses `combine` but relies on `store.getState()` to read values from nested stores. This breaks fine-grained reactivity: the derived store only updates if the _list of instances_ changes, not when the _inner store_ of an instance updates.

**Proposed Fix**:

- **Higher-Order Stores**: Implement a custom Effector store (or usage of `flatten` pattern) that correctly subscribes to the nested store when the ID/Path resolves to one.
- **Workaround**: For the prototype, force updates by ensuring the parent object reference changes (immutable updates) even for inner store changes, or use `watch` to trigger manual updates.

### 4. Memory Leaks in Proxies

**Severity**: Medium
**Location**: `packages/core-experimental/src/keyval.ts`
**Issue**:
Every call to `getItem` (and every property access on the returned proxy) creates new `Event`, `Effect`, and `sample` instances. In React components, this can lead to a massive explosion of units if not memoized.

**Proposed Fix**:

- **Cache Proxies**: Implement a `WeakMap` cache in `keyval` to return the same Proxy instance for the same ID/Store.
- **Stable Units**: Ensure that accessing `item.facets.user.kick` multiple times returns the _same_ Event reference.

---

## 🛠 Refinements

### 5. Type Safety & HKT

- **Goal**: Remove `as any` casting in `create()` and `keyval()`.
- **Plan**: Implement the "Higher-Kinded Types" emulation (as described in the article) to allow `keyval<UserTrait>` to correctly infer the shape of the union.

### 6. Lifecycle Management (✅ Completed)

- **Goal**: Proper cleanup of models.
- **Plan**: Ensure that removing an item from `keyval` triggers `clearNode` on the associated instance and its scope, preventing memory leaks of Effector units.

### 7. Performance Optimization

- **Goal**: O(1) complexity.
- **Plan**: Replace the dynamic `combine` multiplexers in `create()` with a static analysis approach (or linearized graph) where possible, to avoid re-evaluating the entire list for every update.
55 changes: 55 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Implementation Plan: Effector Models Research & Optimization

This document outlines the strategy for implementing the finalized Effector Models API, refining the research examples, and optimizing the build stack for maximum performance.

## 1. Core API Completion (`packages/core-experimental`)

### Operator Implementation

- **`implement(trait, implementation)`**: Create a formal helper to link abstract traits with concrete reactive logic.
- **`ref.self`**: Support recursive model definitions (e.g., categories with subcategories).
- **`ref.tag(name)`**: Support internal cross-references within traits for units that depend on each other.
- **Improved Lenses**: Enhance Proxy-based path resolution in `select().path()` for cleaner deep state access.

### Lifecycle & Multiplexing

- **Variant Lifecycle**: Ensure `enter` and `leave` events are reliably triggered during variant transitions.
- **Reactive Multiplexing**: Optimize how stores and events are swapped when variants change to ensure zero glitches.

## 2. Research Examples Refinement (`apps/models-research`)

### Game Model Demo

- Update `gameModel` to use the finalized `implement` and `variant` APIs.
- Refine `statsModel` to demonstrate robust lifecycle event consumption.

### User Union Demo

- Finalize the polymorphic `usersList` implementation.
- Demonstrate advanced `match` usage for variant-specific business logic.

## 3. Performance & Build Stack Upgrade

### Tooling

- **Vite 6/8 Beta**: Upgrade the dev server and bundler.
- **OXC**: Integrate `@vitejs/plugin-react` with OXC for ultra-fast transpilation.
- **Rolldown**: Switch to Rolldown for production builds to achieve the target 2x speedup.
- **Dev Mode Optimization**: Enable optimized bundling in dev to prevent excessive file requests.

## 4. Testing & Quality Assurance

### Coverage Goal: 100%

- **Unit Tests**: Full coverage of all core operators in `core-experimental`.
- **Integration Tests**: End-to-end business logic verification for Game and User examples.
- **React Integration**: Verify UI synchronization using `effector-react` with `fork` scopes.
- **Tooling**: Use `vitest` with `v8` coverage reporting.

## 5. Implementation Roadmap

1. **Phase 1: API Core**: Implement `implement`, `ref.self`, and `ref.tag`.
2. **Phase 2: Build Upgrade**: Update Vite, OXC, and Rolldown configuration.
3. **Phase 3: Example Refinement**: Update Game and User demos to use the new API.
4. **Phase 4: Test Suite Expansion**: Write exhaustive tests to reach 100% coverage.
5. **Phase 5: Final Verification**: Run full build and test suite to ensure "green" status.
93 changes: 93 additions & 0 deletions PRESENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Effector Models Research Playground

This project implements the experimental **Effector Models API** (Traits, Models, Variants) as described in the "Effector Models" article. It serves as a proof-of-concept to validate the API ergonomics and explore internal implementation challenges.

## 🚀 Getting Started

1. **Install dependencies**:
```bash
pnpm install
```
2. **Run the Playground**:
```bash
npx nx serve models-research
```
3. **Open Browser**:
Navigate to `http://localhost:4200` (or the port shown in the terminal).

---

## 📂 Project Structure

- **`packages/core-experimental`**: The implementation of the experimental API (`model`, `trait`, `define`, `keyval`, `select`, `match`).
- **`apps/models-research`**: The playground application containing the examples.

### Examples

#### 1. Game Model (Variants & Lifecycle)

_Location: `apps/models-research/src/game/`_

Demonstrates how a model can change its internal structure and behavior based on state.

- **`gameModel`**: Has 3 variants (`winning`, `losing`, `draw`).
- When `losing`, it dynamically creates an `$intensity` store.
- Uses `facets.visual` to expose a `$color` that changes based on the variant.
- **`statsModel`**: A separate model that "watches" the `gameModel`.
- Demonstrates **Lifecycle Events**: Listens to `game.variant.losing.enter` and `leave` to start/stop a timer.
- The timer _only_ runs when the game is in the "losing" state.

**🎮 Demo Action**:

- Go to the "Game Model" tab.
- Move the score slider below 0.
- Observe the box turn red (intensity increases with negative score).
- Observe the timer starting.
- Move the slider back to > 0. The timer stops.

#### 2. Chat User (Polymorphism & Unions)

_Location: `apps/models-research/src/user/`_

Demonstrates handling lists of heterogeneous models (Polymorphism).

- **`guestModel`**: Simple user with just a nickname.
- **`memberModel`**: User with a nickname AND a role (`admin` | `user`).
- **`userUnion`**: Combines them into a single type.
- **`usersList`**: A `keyval` list that holds `userUnion` instances.

**Features**:

- **Polymorphic Actions**:
- `kick` (Common facet): Works on any user.
- `promote` (Specific facet): Only works on members.
- **`match()`**: Used to safely route the `promote` action only to `member` variants.
- **Lenses (`select()`)**:
- Demonstrates safely extracting data (Role) from a specific variant, with a fallback if the variant doesn't match.

**🎮 Demo Action**:

- Go to the "Chat User" tab.
- Add a "Guest" and a "Member".
- Select a user to see details (Role).
- Try to "Promote" a Guest (Check console for error).
- "Kick" works for everyone.

---

## 🛠️ Internal Implementation Notes

### The "Runtime" (`packages/core-experimental`)

This prototype uses a dynamic runtime approach to emulate the proposed static graph behavior.

- **`create()`**: The factory that instantiates models. It handles the "Multiplexing" of facets, ensuring that `model.facets.visual.$color` is a valid store that switches its source based on the active variant.
- **`match()`**: Implemented using a Proxy trap. Accessing a property inside the `match` callback automatically creates a reactive link between the trigger event and the target method.
- **`select()`**: Implements a "Lens" pattern. It returns a Store that dynamically looks up values in the instance map.
- **Reactivity**: Now uses a robust **Subscription Manager** pattern (via `watch` and dynamic re-subscription) to ensure deep reactivity. Updates to nested stores propagate instantly, even if the list structure remains static.

## 🔮 Next Steps

1. **Higher-Kinded Types (HKT)**: Implement the TypeScript HKT emulation to improve type inference for generics (as mentioned in the article).
2. **Linearized Runtime**: Replace the dynamic `combine` multiplexers with a compiled static graph for performance.
3. **Beta Release**: Prepare documentation and examples for a wider public release.
43 changes: 43 additions & 0 deletions TESTING_PLAN_DEMO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Comprehensive Testing Plan for Recursive File System Demo

This plan outlines the strategy to achieve 100% test coverage for the features described in [`DEMO_RD.md`](DEMO_RD.md) and implemented as per [`DEMO_IMPL.md`](DEMO_IMPL.md).

## 1. Overview of Testable Components

The testing will be split into two main layers:

1. **Logic Layer (Unit Tests)**: Verifying Effector models, facets, and their interactions.
2. **View Layer (Integration Tests)**: Verifying React components, user interactions, and reactive UI updates.

## 2. Logic Layer: `apps/models-research/src/tree/__tests__/model.test.ts`

Goal: 100% coverage of [`model.ts`](apps/models-research/src/tree/model.ts) and [`facets.ts`](apps/models-research/src/tree/facets.ts).

| Feature | Test Case | Target |
| :---------------- | :--------------------------------------------------------------------- | :------------------------- |
| **Selection** | Verify `$isSelected` becomes true when `select` is called. | `fileModel`, `folderModel` |
| **Selection** | Verify `$selectedId` updates correctly in the shared scope. | Global state / Input |
| **Renaming** | Verify `$name` updates when `rename` event is triggered. | `nodeFacet` |
| **Recursion** | Verify `folderModel` correctly holds and renders `children` instances. | `folderFacet` |
| **Internal Refs** | Verify `visualFacet.$backgroundColor` reacts to `$isSelected`. | `visualFacet` |
| **Initial State** | Verify default selection (root node) and initial expansion state. | Models |

## 3. View Layer: `apps/models-research/src/tree/__tests__/view.test.tsx`

Goal: 100% coverage of [`view.tsx`](apps/models-research/src/tree/view.tsx) and integration logic in [`TreeDemo.tsx`](apps/models-research/src/app/TreeDemo.tsx).

| Interaction | Expected Behavior | Component |
| :---------------------- | :----------------------------------------------------------- | :----------------------- |
| **Single Click** | Selects the node (highlights background). | `FileView`, `FolderView` |
| **Arrow Click** | Toggles folder expansion WITHOUT changing selection. | `FolderView` |
| **Folder Name Click** | Selects folder WITHOUT toggling expansion. | `FolderView` |
| **Double Click** | Enters edit mode, sets local state, and selects node. | `FileView`, `FolderView` |
| **Rename (Enter/Blur)** | Commits name change to model, exits edit mode. | `FileView`, `FolderView` |
| **Rename (Escape)** | Cancels name change, restores old name, exits edit mode. | `FileView`, `FolderView` |
| **Breadcrumbs** | Updates path reactively when a node in the trace is renamed. | `Breadcrumbs` |
| **Details View** | Displays correct name and type for the selected node. | `DetailsView` |

## 4. Coverage Verification

We will use Vitest's built-in coverage tool to verify 100% coverage.
Command: `pnpm vitest run --coverage --project models-research`
Loading
Loading