# Tracking

> trackRender records the paths a consumer reads through a proxy. Covers ProxyCache, raw, hazards, ancestor wake-ups and diff helpers.

Source: https://dirtytalk.42.dog/docs/structural/tracking/

Tracking records which paths a consumer reads, so the container can wake it only when one of those paths changes. `trackRender` is the primitive; [`useStructural`](/docs/structural/react/) is built on it.

## trackRender

```ts
import { StructuralContainer, trackRender } from '@dirtytalk/structural';

class Profile extends StructuralContainer<{
  user: { name: string; email: string };
}> {
  constructor() {
    super({ user: { name: 'Ada', email: 'ada@example.com' } });
  }
}

const profile = new Profile();
const { value, paths, disarm } = trackRender(profile.state, profile.interner);

value.user.name; // records "user.name"
disarm();

console.log([...paths].map((id) => profile.interner.lookup(id))); // ['user.name']
```

Signature:

```ts
function trackRender<S>(
  state: S,
  interner: PathInterner,
  proxyCache?: ProxyCache,
): TrackResult<S>;
```

### Return value

| Field    | Type          | Description                                                                                                                                    |
| -------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `value`  | `S`           | A recording proxy over `state`. Primitives, `null` and `undefined` are returned as-is.                                                         |
| `paths`  | `Set<PathId>` | Grows as the proxy is read. Always a `Set`, never `ALL_PATHS`.                                                                                 |
| `disarm` | `() => void`  | Stops recording. Afterwards every read on this proxy tree returns the raw value and records nothing. Call it when the synchronous render ends. |

### Recording rules

- Own, string-keyed property reads record a path. Symbol keys and inherited properties (array methods, prototype getters) do not.
- Recording is leaf-only: reading `a.b.c` leaves just `a.b.c` in the set; each deeper read drops its immediate parent. A consumer of `user.name` does not wake when a sibling `user.email` changes. A consumer that reads the whole `user` object without going deeper keeps `user` and wakes on any change under it.
- Plain objects and arrays are wrapped in child proxies. `Map`, `Set`, `Date` and class instances are leaves, returned raw; a change to one is a reference change at its path.
- A non-writable, non-configurable property (a field of an `Object.freeze`d state) is returned raw, because the Proxy invariant requires it.
- Arrays: `map`, `filter`, `for..of` and spread run against the proxy, so `items.map((i) => i.title)` records `items.length`, `items.0.title`, `items.1.title`, and so on.
- `includes`, `indexOf` and `lastIndexOf` run against the raw array and pin the array's own path as a content dependency.
- `Object.keys(obj)`, `for..in` and object spread pin the object's path (the root id for the root object), so the consumer wakes on key add and remove.
- `key in obj` records `obj.key`, so the consumer wakes when the key is added or removed.
- A proxy is cached per `(target, prefix)` within one call, so `value.user === value.user` holds within a render.
- In development (`NODE_ENV !== 'production'`), `set`, `deleteProperty` and `defineProperty` on a tracked proxy throw: state is read-only outside the container.

## PathInterner

`PathInterner` maps dotted path strings to numeric `PathId`s. Each container class has one, available as `container.interner`. Pass it to `trackRender` so recorded ids match the ones the container marks.

```ts
import { PathInterner } from '@dirtytalk/structural';

const interner = new PathInterner();
const id = interner.intern('user.name');
interner.lookup(id); // 'user.name'
interner.size; // 1
```

Array indices appear as numeric segments (`items.0.title`). See the [API reference](/docs/structural/api/#pathinterner) for all methods.

## raw and untracked

```ts
function raw<T>(v: T): T;
const untracked: typeof raw;
```

`raw(v)` returns the underlying target of a recording proxy, or `v` itself when it is not a proxy. `untracked` is the same function under a name that states intent: pass a snapshot across a component boundary so the child's reads are not attributed to the parent.

```tsx
import { raw, untracked } from '@dirtytalk/structural';
import { useStructural } from '@dirtytalk/structural/react';
import { Todos } from './todos';

const todos = new Todos();

function List() {
  const [state] = useStructural(todos);
  const first = state.todos[0];
  const same = raw(first) === todos.state.todos[0]; // true
  return <Row item={untracked(first)} />;
}
```

A child receiving `untracked(item)` is not reactive to that value unless it reads from the container through its own hook.

## ProxyCache

```ts
class ProxyCache {}
```

A caller-owned cache that lets `trackRender` reuse the same proxy for a `(target, prefix)` pair across calls, instead of allocating a new one per call. Pass it as the third argument.

```ts
import { ProxyCache, trackRender } from '@dirtytalk/structural';
import { Todos } from './todos';

const todos = new Todos();
const cache = new ProxyCache();

const first = trackRender(todos.state, todos.interner, cache);
first.value.todos.length;
first.disarm();

const second = trackRender(todos.state, todos.interner, cache);
second.value.todos.length;
second.disarm();
```

Rules:

- **Keyed by `(target, prefix)`, not target alone.** The same object read at two paths gets two independent proxies. A reused proxy requires the same object at the same path as last time, so reordering an item to a new index allocates a fresh proxy for it, while items that keep their index are reused.
- **One cache per call site.** Use one per hook instance or per resolved dependency, never one global or per-container cache. An entry holds one live session at a time; sharing a cache between independently timed callers lets one caller's render repoint an entry the other is still reading. If a consumer tracks several independent things per render, give each its own cache.
- **No eviction.** The cache is weakly keyed by target, so unreferenced objects are collected.
- **The root proxy is never reused.** Only nested proxies are. This keeps `useMemo(..., [state])` patterns recording correctly.
- `disarm()` prunes stale entries for objects read at shifting prefixes. Call it when each render ends.
- `raw()` still unwraps reused proxies.

## Tracking hazards

Values read off a recording proxy are themselves proxies. Two situations need `raw()`:

1. **Identity comparisons.** Comparing a value read from the proxy with a raw reference using `===`, or with an identity search such as `Array.prototype.includes` on a derived array, fails because the value is wrapped. Compare `raw(value) === rawRef`.
2. **Escaped proxies.** A proxy, or a sub-proxy inside a derived array such as a `.filter` result, that outlives the render and is armed keeps recording into a stale `paths` set and breaks identity against the state. Call `raw()` on anything stored or handed to code expecting plain objects. After `disarm()`, reads pass through without recording, but identity still differs from the raw object.

```ts
import { raw } from '@dirtytalk/structural';
import { Todos } from './todos';

const todos = new Todos();
declare const trackedTodos: Todos['state']['todos']; // read off a tracked proxy

const wasFiltered = trackedTodos.filter((t) => !t.done).map((t) => raw(t));
const index = todos.state.todos.indexOf(raw(trackedTodos[0]));
```

> **Warning:** Writes to a tracked proxy throw in development and are silently applied to the underlying object in production. Mutate state only through the container.

## Ancestor wake-ups

When `patch` replaces an array, `null`, or a class instance wholesale, the diff cannot see inside it. A consumer that read only a descendant (`todos.length`, `profile.bio`) would have nothing to match. The container handles this with a second kind of id, an ancestor-watch id:

- `patch` marks a normal id for the replaced path plus an ancestor-watch id for it. Plain-object branches get no ancestor-watch id, so a `user.name` change does not wake a `user.email` reader.
- Before marking, the container refines each ancestor-watch id: it diffs the skeleton below that path and marks the descendant paths whose own value changed. Registered consumers therefore wake only when what they read changed.
- `expandWithAncestors(paths, interner)` adds the ancestor-watch ids of every ancestor of each leaf to a path set. [`useStructural`](/docs/structural/react/) does not need it, because refinement runs in the container. Use it for a raw `channel.subscribe` consumer whose interest should wake on an atomic replacement of any ancestor.

```ts
import { expandWithAncestors, trackRender } from '@dirtytalk/structural';
import { Todos } from './todos';

const todos = new Todos();
const { value, paths, disarm } = trackRender(todos.state, todos.interner);
value.todos.length;
disarm();

const interest = expandWithAncestors(paths, todos.interner);
todos.subscribe(
  () => interest,
  () => {},
);
```

## Diff helpers

These are the building blocks the container uses. Use them for custom containers or tests.

```ts
import {
  StructuralContainer,
  diffAlongSkeleton,
  getAt,
} from '@dirtytalk/structural';

class Pair extends StructuralContainer<{ a: number; b: number }> {
  constructor() {
    super({ a: 1, b: 1 });
  }
}

const pair = new Pair();
const prev = { a: 1, b: 1 };
const next = { a: 2, b: 1 };
const skeleton = new Set([
  pair.interner.intern('a'),
  pair.interner.intern('b'),
]);

const dirty = diffAlongSkeleton(prev, next, skeleton, pair.interner);
// dirty contains the id of 'a' only
getAt(next, 'a'); // 2
```

- `diffAlongSkeleton(prev, next, skeleton, interner, equalsAt?)` returns the skeleton ids whose values differ. An `ALL_PATHS` skeleton returns `ALL_PATHS`.
- `changedPathsFromPatch(prev, next, patch, interner, equalsAt?)` returns the paths from a patch whose values changed, plus ancestor-watch ids for atomic replacements.
- `getAt(state, 'a.b.0')` reads a value at a dotted path and returns `undefined` for missing intermediates.

See the [API reference](/docs/structural/api/#diffing) for signatures.

## Debug hooks

> **Note:** `__setTrackTrace` and `__setPersistTrackingProxies` are internal, test-only seams. They are exported but not part of the stable API.

- `__setTrackTrace(hook | null)` installs a global hook that receives a `TrackTraceEvent` for every recording decision (`root-wrap`, `record`, `drop-parent`, `pin`, `ownKeys`, `has`, `pass-through`, `disarm`).
- `__setPersistTrackingProxies(boolean | null)` forces whether `trackRender` honors a supplied `ProxyCache`. `null` restores the default, which is enabled.
