Skip to content

State Accessor ​

What It Is ​

State Accessor shows how an application can replace the built-in dotted-path resolver used by scatter source reads and gather writes. The example wires DottedPathAccessor and a custom PrefixAccessor into a Dagonizer instance.

Use this when state is namespaced, wrapped, proxied, or backed by a structure that is not a plain JavaScript object path.

How It Works ​

The dispatcher calls the configured StateAccessorInterface whenever it reads a scatter source path or writes gather output. PrefixAccessor prepends a namespace before delegating to DottedPathAccessor, so the DAG can keep short path names while the host stores data under a scoped subtree.

Diagrams, Examples, and Outputs ​

This page has no DAG diagram because it demonstrates accessor replacement, not a routed graph. The CLI output shows direct reads/writes, prefixed reads/writes, and dispatcher construction with the custom accessor.

Run ​

bash
npx tsx examples/state-accessor.ts

What It Lets You Do ​

Custom state accessors let applications change how scatter source reads and gather writes resolve paths in state. Use them when your state is namespaced, wrapped, proxied, or backed by a structure that is not a simple dotted JavaScript object path.

DottedPathAccessor is the built-in path resolver used by scatter source reads and gather writes. Applications implement the StateAccessorInterface contract to replace it. PrefixAccessor (defined in examples/dags/state-accessor.ts) prepends a fixed namespace segment to every key before delegating to DottedPathAccessor.

This example shows:

  1. Direct get/set via DottedPathAccessor on a concrete NodeStateBase.
  2. The same operations through PrefixAccessor.
  3. A Dagonizer constructed with the custom accessor (accessor option).

Code Samples ​

ts
/**
 * state-accessor: demonstrates DottedPathAccessor and the custom PrefixAccessor
 * from dags/state-accessor.ts wired into a Dagonizer instance.
 *
 * DottedPathAccessor is the built-in path resolver used by scatter source reads
 * and gather writes. Consumers implement the StateAccessorInterface contract to replace
 * it. PrefixAccessor (defined in dags/state-accessor.ts) prepends a fixed
 * namespace segment to every key before delegating to DottedPathAccessor.
 *
 * This example shows:
 *   1. Direct get/set via DottedPathAccessor on a concrete NodeStateBase.
 *   2. The same operations through PrefixAccessor.
 *   3. A Dagonizer constructed with the custom accessor (accessor option).
 *
 * DAG definition (ArchiveState, PrefixAccessor, accessors): examples/dags/state-accessor.ts
 *
 * Run: npx tsx examples/state-accessor.ts
 */

import { Dagonizer } from '@studnicky/dagonizer';
import { DottedPathAccessor } from '@studnicky/dagonizer/runtime';
import { ArchiveState, PrefixAccessor } from './dags/state-accessor.js';

process.stdout.write('\n=== StateAccessorInterface: DottedPathAccessor + PrefixAccessor ===\n\n');

// ── 1. DottedPathAccessor: get and set on nested paths ──────────────────────

const dotted = new DottedPathAccessor();
const stateA  = new ArchiveState();

stateA.catalogue = { shelves: { fiction: 'Shelf A' } };

const fiction = dotted.get(stateA, 'catalogue.shelves.fiction');
process.stdout.write(`dotted.get('catalogue.shelves.fiction') = "${String(fiction)}"\n`);

dotted.set(stateA, 'catalogue.shelves.non-fiction', 'Shelf B');
const nonFiction = dotted.get(stateA, 'catalogue.shelves.non-fiction');
process.stdout.write(`dotted.set then get 'catalogue.shelves.non-fiction' = "${String(nonFiction)}"\n`);

// null on miss
const miss = dotted.get(stateA, 'catalogue.shelves.missing');
process.stdout.write(`dotted.get on missing path = ${JSON.stringify(miss)}\n`);

// ── 2. PrefixAccessor: every key is automatically prefixed ──────────────────

const prefix  = new PrefixAccessor('archivist');
const stateB  = new ArchiveState();

prefix.set(stateB, 'shelves.science', 'Shelf C');
const science = prefix.get(stateB, 'shelves.science');
process.stdout.write(`\nprefix.set('shelves.science') stored at 'archivist.shelves.science'\n`);
process.stdout.write(`prefix.get('shelves.science') = "${String(science)}"\n`);

// raw confirmation: value lives at the prefixed path
const raw = dotted.get(stateB, 'archivist.shelves.science');
process.stdout.write(`raw dotted.get('archivist.shelves.science') = "${String(raw)}"\n`);

// ── 3. Dagonizer constructed with the custom accessor ───────────────────────

const dispatcher = new Dagonizer<ArchiveState>({ accessor: prefix });
process.stdout.write(`\nDagonizer constructed with PrefixAccessor('archivist')\n`);
process.stdout.write(`typeof dispatcher = ${typeof dispatcher}\n`);

process.stdout.write('\nLesson: implement StateAccessorInterface to swap the path resolver;\n');
process.stdout.write('        scatter reads and gather writes use the custom accessor.\n');

// #region gather-strategy
import {
  GatherStrategies,
  GatherStrategy,
  Batch,
} from '@studnicky/dagonizer';
import type { GatherRecordType, NodeStateInterface } from '@studnicky/dagonizer';
import type { GatherConfigType } from '@studnicky/dagonizer/entities';
import type { StateAccessorInterface } from '@studnicky/dagonizer/contracts';

class AverageGather extends GatherStrategy {
  readonly name = 'average';
  readonly '@id' = 'urn:noocodec:node:average';
  reduce(
    config: GatherConfigType,
    batch: Batch<GatherRecordType>,
    state: NodeStateInterface,
    accessor: StateAccessorInterface,
  ): void {
    if (config.target === undefined) return;
    const all: number[] = [];
    for (const item of batch) {
      const raw = accessor.get(item.state.cloneState, config.field ?? 'score');
      all.push(typeof raw === 'number' ? raw : 0);
    }
    const avg = all.reduce((a, b) => a + b, 0) / Math.max(1, all.length);
    accessor.set(state, config.target, avg);
  }
}

// Register with the engine so it's available in DAG topology configs.
GatherStrategies.register(new AverageGather());
// #endregion gather-strategy

Details for Nerds ​

  • DottedPathAccessor. The built-in implementation. get(target, 'a.b.c') reads target.a.b.c using dot-segment traversal. set(target, 'a.b.c', value) writes at the same path, creating intermediate objects as needed.
  • StateAccessorInterface contract. Two methods: get<T>(target, path): T | null and set(target, path, value): void. Implement both to replace the built-in resolver.
  • PrefixAccessor. Prepends a fixed namespace to every path before delegating. Useful when a dispatcher instance manages a namespaced sub-tree of a larger state object — all scatter reads and gather writes land in the namespace automatically.
  • accessor option. Pass a StateAccessorInterface implementation as accessor: in the Dagonizer constructor. The dispatcher uses it for all scatter source reads and gather writes in that instance.

Watched over by the Order of Dagon.