# The Config System & Circular Dependencies

With the class blueprint compiled and our reactive API (getters and setters) generated by `Neo.setupClass`, we face a complex runtime challenge. Complex user interfaces have intertwined state. Updating these properties naively leads to circular dependencies, infinite loops, and broken lifecycle hooks. 

The Neo.mjs runtime engine provides a robust, predictable Config System designed specifically to handle complex state mutations gracefully.

## The Trailing Underscore Convention

As established during the compilation phase, the core mechanism for defining a reactive property is the trailing underscore in the `static config` block:

```javascript readonly
class MyComponent extends Base {
    static config = {
        // Non-reactive (applied directly to prototype)
        className: 'MyApp.MyComponent',
        
        // Reactive (engine generates getter/setter and hooks)
        title_: 'Default Title',
        isActive_: false
    }
}
```

## Config Descriptors

Sometimes, a simple default value isn't enough. You might need to instruct the engine on *how* to handle a specific config when it is merged, cloned, or compared for equality. For advanced use cases, Neo.mjs allows you to define a reactive config using a **Descriptor Object** instead of a primitive value.

You create a descriptor using the `[Neo.core.ConfigSymbols.isDescriptor]` symbol:

```javascript readonly
import {isDescriptor} from '../core/ConfigSymbols.mjs';

class MyList extends Component {
    static config = {
        // A complex reactive config defined via a descriptor
        items_: {
            [isDescriptor]: true,
            clone         : 'shallow',    // How to clone the value when setting
            cloneOnGet    : 'none',       // How to clone the value when reading
            isEqual       : () => false,  // Custom equality function
            merge         : 'deepArrays', // Strategy for merging configs during setup
            value         : []            // The actual default value
        }
    }
}
```

When `Neo.setupClass` encounters a descriptor, it extracts the rules and uses the `value` property as the actual default. The underlying `Neo.core.Config` instance then uses these rules to govern how the property behaves at runtime (e.g., bypassing deep equality checks for performance, or defining custom array merging logic).

## Push-Based Lifecycle Hooks

When you assign a new value to a reactive config at runtime (`this.title = 'New Title'`), the generated setter automatically pushes that change through a sequence of optional lifecycle hooks:

1. **`beforeSetTitle(value, oldValue)`**: Runs *before* the value is applied. This is the gatekeeper. It is useful for validation or data transformation. Returning `undefined` from this hook cancels the update entirely.
2. **`afterSetTitle(value, oldValue)`**: Runs *after* the value is successfully applied and validated. This is where you trigger imperative side effects (e.g., updating the DOM or fetching new data).

### The `undefined` Sentinel Value

In Neo.mjs, `undefined` is a strict sentinel value indicating "initial instantiation".

When an instance is constructed, its `afterSet` hooks will fire for the very first time as default values are applied. In this initial execution, the `oldValue` argument will *always* be exactly `undefined`.

```javascript readonly
afterSetTitle(value, oldValue) {
    if (oldValue !== undefined) {
        // This logic will ONLY run on subsequent runtime updates,
        // safely skipping the initial setup phase.
        this.update();
    }
}
```

Because of this specific architectural meaning, **you should never set a config to `undefined` after it has been initialized.** If you need to clear a value or reset state, explicitly use `null`.

## Solving Circular Dependencies (The `configSymbol`)

The true test of a config system is how it handles batch updates where configs depend on each other. Imagine setting two configs at the same time where the validation logic of one depends on the *new* value of the other.

### The Temporary Holding Zone

`src/core/Base.mjs` solves this using a `configSymbol` (a hidden ES6 `Symbol` property on the instance) as a temporary holding zone.

When you call `this.set()` to perform a batch update with multiple properties, all new reactive values are placed into `this[configSymbol]`. The engine then processes them sequentially. 

The brilliance of this mechanism lies in the generated *getter*. The getter for any reactive config is programmed to check `this[configSymbol]` *before* checking the internal backing property. As a result, any lifecycle hook will immediately receive the *new, pending* value of a sibling config, even if that property hasn't fully processed its own `beforeSet`/`afterSet` cycle yet.

### Example: Cross-Dependent Configs

Consider this simplified scenario based on `Neo.examples.core.config.MainContainer`:

```javascript live-preview
import Base from '../core/Base.mjs';

class MathComponent extends Base {
    static config = {
        className: 'MathComponent',
        a_: null,
        b_: null
    }

    afterSetA(value, oldValue) {
        if (oldValue !== undefined) {
            // Even though `afterSetA` might run before `b` has finished its setter,
            // `this.b` will correctly return the NEW value passed in the set() block.
            console.log(`afterSetA: a + b = ${value + this.b}`);
        }
    }

    afterSetB(value, oldValue) {
        if (oldValue !== undefined) {
            console.log(`afterSetB: b + a = ${value + this.a}`);
        }
    }
}

let comp = Neo.create(MathComponent);

// Trigger a batch update
comp.set({
    a: 5,
    b: 5
});

// Console Output:
// afterSetA: a + b = 10
// afterSetB: b + a = 10
```

Without the `configSymbol` buffering these values, when `afterSetA` runs, `this.b` would still be `null`, resulting in a broken calculation. The holding zone guarantees that during a batch `set()`, all hooks operate against the "future state" of the instance, eliminating timing bugs and resolving circular read dependencies gracefully.

### Preventing Infinite Write Loops
While the `configSymbol` solves circular *reads* safely, what happens if `afterSetA` *writes* to `b`, and `afterSetB` *writes* to `a`? Could they bounce back and forth infinitely?

This is where the low-level `Neo.core.Compare` utility acts as the ultimate safety net. Before an `afterSet` hook is fired, the generated setter passes the new value through the deep comparison engine. **An `afterSet` hook is ONLY executed if the value has *actually* changed.** If your mathematical calculations stabilize and produce the same result, the loop is broken immediately.

The `configSymbol` elegantly solves local component state mutations. But this push-based model requires writing explicit `afterSet` hooks. As an application scales, manually wiring hooks to synchronize global state leads to unmaintainable "spaghetti code." The core engine needs a brain to track dependencies automatically.
