# API reference

> Every public export of @dirtytalk/structural and @dirtytalk/structural/react with TypeScript signatures, parameters, returns and examples.

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

Reference for all exports of `@dirtytalk/structural` (core) and `@dirtytalk/structural/react`. For explanations see [Containers](/docs/structural/containers/), [Tracking](/docs/structural/tracking/) and [React](/docs/structural/react/).

## Container

### StructuralContainer

```ts
abstract class StructuralContainer<S> {
  static getInternerFor(ctor: object): PathInterner;
  constructor(initial: S, options?: StructuralContainerOptions);
  get state(): S;
  get interner(): PathInterner;
  get channel(): DirtyChannel<PathSet>;
  get consumerCount(): number;
  getConsumerPaths(): ReadonlyMap<ConsumerId, PathSet>;
  dispose(): void;
  subscribe(interest: () => PathSet, cb: (dirty: PathSet) => void): () => void;
  subscribePaths(cb: () => void): PathSubscription;
  registerConsumerPaths(id: ConsumerId, paths: PathSet): void;
  unregisterConsumer(id: ConsumerId): void;
  protected emit(next: S): void;
  protected patch(partial: DeepPartial<S>): void;
  protected update(fn: (state: S) => S): void;
}
```

Base class for a state container. Abstract: extend it and expose methods that call the protected mutators.

- `getInternerFor(ctor)` returns the per-class interner, creating it on first use. `interner` resolves it for `this.constructor`.
- `state` is the current state. `channel` is the underlying `DirtyChannel<PathSet>`.
- `consumerCount` counts consumers registered through `registerConsumerPaths`.
- `getConsumerPaths()` returns a copy of the registered consumers' path sets.
- `dispose()` disposes the channel; later marks are ignored.
- `subscribe` is a pass-through to `channel.subscribe`. Returns an unsubscribe function.
- `subscribePaths` returns a [`PathSubscription`](#pathsubscription). Its interest starts empty.
- `registerConsumerPaths` stores a snapshot of `paths` for `id` and updates the observed skeleton. Re-registering an equal set is a no-op.
- `unregisterConsumer` removes `id`; unknown ids are ignored.
- `emit(next)`: replace state. No-op when `Object.is(state, next)`. Diffs along the skeleton, or marks `ALL_PATHS` when no consumer is registered or one registered `ALL_PATHS`.
- `patch(partial)`: deep-merge plain objects, replace everything else. No-op when nothing changes. Marks changed paths only.
- `update(fn)`: `emit(fn(state))`.

Throws: nothing of its own. Subscriber errors go to `onError`, or are re-thrown in the scheduler callback.

```ts
import { SyncScheduler } from '@dirtytalk/engine';
import { StructuralContainer } from '@dirtytalk/structural';

class Counter extends StructuralContainer<{ count: number }> {
  constructor() {
    super({ count: 0 }, { scheduler: new SyncScheduler() });
  }
  increment() {
    this.update((s) => ({ ...s, count: s.count + 1 }));
  }
}
```

### StructuralContainerOptions

```ts
interface StructuralContainerOptions {
  scheduler?: Scheduler;
  equality?: ReadonlyMap<string, (a: unknown, b: unknown) => boolean>;
  onError?: (err: unknown) => void;
}
```

- `scheduler`: scheduler for the channel. Default is a shared `MicrotaskScheduler`.
- `equality`: custom equality keyed by exact dotted path. Unlisted paths use `Object.is`.
- `onError`: receives errors thrown by subscribers or interest thunks during a flush.

```ts
import { SyncScheduler } from '@dirtytalk/engine';
import type { StructuralContainerOptions } from '@dirtytalk/structural';

const options: StructuralContainerOptions = {
  scheduler: new SyncScheduler(),
  equality: new Map([
    ['price', (a, b) => Math.abs(Number(a) - Number(b)) < 0.01],
  ]),
  onError: (err) => console.error(err),
};
```

### DeepPartial

```ts
type DeepPartial<T> =
  T extends ReadonlyArray<infer U>
    ? ReadonlyArray<DeepPartial<U>>
    : T extends Date | Map<unknown, unknown> | Set<unknown> | RegExp
      ? T
      : T extends object
        ? { [K in keyof T]?: DeepPartial<T[K]> }
        : T;
```

Argument type of `patch`. Plain-object branches are optional recursively; arrays and built-in value types are taken whole.

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

type State = { user: { name: string; age: number } };
const patch: DeepPartial<State> = { user: { name: 'Ada' } };
```

### PathSubscription

```ts
interface PathSubscription {
  setInterest(interest: PathSet): void;
  unsubscribe(): void;
}
```

Returned by `subscribePaths`. `setInterest` replaces the watched paths (do not mutate the set afterwards; passing the same set is a no-op). `unsubscribe` is idempotent.

```ts
import type { PathSubscription } from '@dirtytalk/structural';
import { ALL_PATHS } from '@dirtytalk/structural';

declare const sub: PathSubscription;
sub.setInterest(ALL_PATHS);
sub.unsubscribe();
```

## Paths

### PathId

```ts
type PathId = number;
```

Interned identifier for a path. Stable per container class.

### ConsumerId

```ts
type ConsumerId = string | symbol;
```

Opaque consumer identifier used by `registerConsumerPaths`.

### PathInterner

```ts
class PathInterner {
  intern(path: string): PathId;
  internChild(parentId: PathId, key: string): PathId;
  childIds(parentId: PathId): ReadonlyMap<string, PathId>;
  internAncestor(path: string): PathId;
  internAncestorOf(realId: PathId): PathId;
  ancestorWatchIds(id: PathId): readonly PathId[];
  ancestorTargetId(id: PathId): PathId | undefined;
  lookupSegments(id: PathId): readonly string[];
  rootId(): PathId;
  isRootId(id: PathId): boolean;
  lookup(id: PathId): string;
  isAncestorId(id: PathId): boolean;
  get size(): number;
}
```

Append-only mapping between dotted path strings and `PathId`s.

- `intern(path)`: id for a dotted path (`'user.name'`, `'items.0.title'`); the same string always returns the same id.
- `internChild(parentId, key)`: `intern` of `parent.key` without building the string. Use `-1` as `parentId` for a top-level key.
- `childIds(parentId)`: live map of child ids interned so far.
- `internAncestor(path)` / `internAncestorOf(realId)`: the ancestor-watch id of a path.
- `ancestorWatchIds(id)`: ancestor-watch ids of every non-root ancestor of `id`, longest first.
- `ancestorTargetId(id)`: the real path id behind an ancestor-watch id, else `undefined`.
- `lookupSegments(id)`: memoized path segments (`[]` for the root).
- `rootId()` / `isRootId(id)`: the reserved root id, which decodes to `''`.
- `lookup(id)`: path string for an id. Ancestor-watch ids decode to their real path, the root id to `''`.
- `isAncestorId(id)`: true for ancestor-watch ids.
- `size`: number of interned entries, including ancestor-watch ids and sentinels.

Throws: `lookup` throws `RangeError` for an unknown id. A dev-mode warning is logged at 5000 entries.

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

const interner = new PathInterner();
const id = interner.intern('user.name');
interner.lookup(id); // 'user.name'
interner.internChild(interner.intern('user'), 'email') ===
  interner.intern('user.email'); // true
```

### PathSet

```ts
type PathSet = Set<PathId> | AllPaths;
```

A set of path ids, or `ALL_PATHS`. The dirty region type of a container's channel.

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

const paths: PathSet = new Set([0, 1]);
```

### AllPaths

```ts
type AllPaths = typeof ALL_PATHS;
```

Type of the `ALL_PATHS` sentinel.

### ALL_PATHS

```ts
const ALL_PATHS: unique symbol; // Symbol.for('@dirtytalk/structural/ALL_PATHS')
```

Sentinel that intersects every non-empty path set. As an interest it means "wake on any change". As a dirty set it means "everything may have changed".

```ts
import { ALL_PATHS, PathSetSpace } from '@dirtytalk/structural';

PathSetSpace.intersects(ALL_PATHS, new Set([1])); // true
```

### emptyPathSet

```ts
function emptyPathSet(): PathSet;
```

Returns a new empty `Set<PathId>`.

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

const none = emptyPathSet();
```

### pathSetUnion

```ts
function pathSetUnion(a: PathSet, b: PathSet): PathSet;
```

Union of two path sets. Returns `ALL_PATHS` if either is. May return one input by reference when the other is empty; otherwise a new set.

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

pathSetUnion(new Set([1]), new Set([2])); // Set { 1, 2 }
```

### pathSetEquals

```ts
function pathSetEquals(a: PathSet, b: PathSet): boolean;
```

True when both are `ALL_PATHS`, or both are sets with the same members.

```ts
import { ALL_PATHS, pathSetEquals } from '@dirtytalk/structural';

pathSetEquals(new Set([1]), new Set([1])); // true
pathSetEquals(ALL_PATHS, new Set([1])); // false
```

### PathSetSpace

```ts
const PathSetSpace: Space<PathSet>; // Space is exported by @dirtytalk/engine
```

The engine `Space` implementation for path sets: `empty`, `isEmpty`, `union`, `unionInto` (in-place accumulate, never mutates its second argument), and `intersects`. Used by the container's `DirtyChannel`.

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

PathSetSpace.intersects(new Set([1, 2]), new Set([2, 3])); // true
```

### expandWithAncestors

```ts
function expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet;
```

Adds an ancestor-watch id for every ancestor of every path in `paths`. Returns `ALL_PATHS` unchanged, and returns `paths` itself when no path has an ancestor. Does not mutate `paths`.

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

const interner = new PathInterner();
const expanded = expandWithAncestors(
  new Set([interner.intern('a.b.c')]),
  interner,
);
// contains 'a.b.c' plus ancestor-watch ids for 'a.b' and 'a'
```

## Tracking

### trackRender

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

Wraps `state` in a recording proxy. Parameters: `state` is the value to read; `interner` interns recorded paths (use the container's); `proxyCache` optionally reuses proxies across calls. Returns a [`TrackResult`](#trackresult). A non-object `state` is returned as-is with an empty path set. In development, writes through the proxy throw `Error`.

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

const interner = new PathInterner();
const { value, paths, disarm } = trackRender({ a: { b: 1 } }, interner);
value.a.b;
disarm();
if (paths !== ALL_PATHS) [...paths].map((id) => interner.lookup(id)); // ['a.b']
```

### TrackResult

```ts
interface TrackResult<S> {
  value: S;
  paths: PathSet;
  disarm: () => void;
}
```

Result of `trackRender`. `paths` is always a `Set<PathId>` at runtime, but is typed `PathSet`. `disarm` stops recording; later reads on the proxy tree pass through.

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

declare const result: TrackResult<{ n: number }>;
result.disarm();
```

### raw

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

Returns the underlying object of a recording proxy; returns `v` unchanged for anything else, including primitives, `null` and `undefined`.

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

declare const tracked: { id: number };
const plain = raw(tracked);
```

### untracked

```ts
const untracked: typeof raw;
```

Alias of `raw`. Use at component boundaries to pass an untracked snapshot.

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

declare const item: { title: string };
const snapshot = untracked(item);
```

### ProxyCache

```ts
class ProxyCache {}
```

Caller-owned cache for reusing proxies across `trackRender` calls, keyed by `(target, prefix)`. Create one per call site. Takes no constructor arguments.

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

const cache = new ProxyCache();
const interner = new PathInterner();
const first = trackRender({ list: [1] }, interner, cache);
first.disarm();
```

### TrackTraceEvent

```ts
interface TrackTraceEvent {
  instance: number;
  kind:
    | 'root-wrap'
    | 'record'
    | 'drop-parent'
    | 'pin'
    | 'ownKeys'
    | 'has'
    | 'pass-through'
    | 'disarm';
  prefix: string;
  key?: string;
  path?: string;
  armed: boolean;
}
```

Event passed to the hook installed with `__setTrackTrace`. Internal and test-only.

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

const log = (e: TrackTraceEvent) => console.log(e.kind, e.path);
```

### \_\_setTrackTrace

```ts
function __setTrackTrace(hook: ((e: TrackTraceEvent) => void) | null): void;
```

Installs (or clears with `null`) a global hook for every recording decision. Internal, test-only.

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

__setTrackTrace((e) => console.log(e.kind));
__setTrackTrace(null);
```

### \_\_setPersistTrackingProxies

```ts
function __setPersistTrackingProxies(v: boolean | null): void;
```

Forces whether `trackRender` honors a `ProxyCache` (`null` restores the default, enabled). Internal, test-only.

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

__setPersistTrackingProxies(false);
__setPersistTrackingProxies(null);
```

## Diffing

### diffAlongSkeleton

```ts
function diffAlongSkeleton<S>(
  prev: S,
  next: S,
  skeleton: PathSet,
  interner: PathInterner,
  equalsAt?: (pathId: PathId, prev: unknown, next: unknown) => boolean,
): PathSet;
```

Returns the ids in `skeleton` whose values differ between `prev` and `next`. `equalsAt` overrides equality per path (default `Object.is`). An `ALL_PATHS` skeleton returns `ALL_PATHS`; an empty one returns an empty set. Pure.

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

const interner = new PathInterner();
const skeleton = new Set([interner.intern('a'), interner.intern('b')]);
diffAlongSkeleton({ a: 1, b: 1 }, { a: 2, b: 1 }, skeleton, interner); // Set { id of 'a' }
```

### changedPathsFromPatch

```ts
function changedPathsFromPatch<S>(
  prev: S,
  next: S,
  patch: Partial<S>,
  interner: PathInterner,
  equalsAt?: (pathId: PathId, prev: unknown, next: unknown) => boolean,
): PathSet;
```

Walks the plain-object branches of `patch` and returns the paths whose values changed between `prev` and `next`. Each changed branch also contributes its own path. For a path replaced atomically (an array, `null`, a class instance) it also adds the path's ancestor-watch id. Unchanged branches are pruned. Returns a new `Set`.

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

const interner = new PathInterner();
const prev = { user: { name: 'Ada', age: 36 } };
const next = { user: { name: 'Grace', age: 36 } };
changedPathsFromPatch(prev, next, { user: next.user }, interner);
// Set { id of 'user', id of 'user.name' }
```

### getAt

```ts
function getAt(state: unknown, path: string): unknown;
```

Reads the value at a dotted path (`'user.email'`, `'items.5.name'`). Returns `undefined` for any missing intermediate and never throws. An empty path returns `state`.

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

getAt({ items: [{ name: 'x' }] }, 'items.0.name'); // 'x'
```

## React

### useStructural

```ts
function useStructural<S, C extends StructuralContainer<S>>(
  container: C,
  options?: UseStructuralOptions,
): readonly [S, C];
```

Imported from `@dirtytalk/structural/react`. Subscribes the component to `container` for the paths read during render and returns `[state, container]`, where `state` is a recording proxy. Requires React 18 or newer. See [React](/docs/structural/react/).

```tsx
import { StructuralContainer } from '@dirtytalk/structural';
import { useStructural } from '@dirtytalk/structural/react';

class Counter extends StructuralContainer<{ count: number }> {
  constructor() {
    super({ count: 0 });
  }
  increment() {
    this.patch({ count: this.state.count + 1 });
  }
}

const counter = new Counter();

export function CountButton() {
  const [state, container] = useStructural(counter);
  return <button onClick={() => container.increment()}>{state.count}</button>;
}
```

### UseStructuralOptions

```ts
interface UseStructuralOptions {
  select?: never;
}
```

Options for `useStructural`. There are no settable options today.

```ts
import type { UseStructuralOptions } from '@dirtytalk/structural/react';

const options: UseStructuralOptions = {};
```

### UseStructuralResult

```ts
interface UseStructuralResult<S, C extends StructuralContainer<S>> {
  0: S;
  1: C;
  readonly length: 2;
  [Symbol.iterator](): IterableIterator<S | C>;
}
```

Tuple-like shape of the hook's result. The hook itself is typed to return `readonly [S, C]`.

```ts
import type { UseStructuralResult } from '@dirtytalk/structural/react';
import type { StructuralContainer } from '@dirtytalk/structural';

type Result = UseStructuralResult<
  { n: number },
  StructuralContainer<{ n: number }>
>;
```
