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
npx tsx examples/state-accessor.tsWhat 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:
- Direct
get/setviaDottedPathAccessoron a concreteNodeStateBase. - The same operations through
PrefixAccessor. - A
Dagonizerconstructed with the custom accessor (accessoroption).
Code Samples
/**
* 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-strategyDetails for Nerds
DottedPathAccessor. The built-in implementation.get(target, 'a.b.c')readstarget.a.b.cusing dot-segment traversal.set(target, 'a.b.c', value)writes at the same path, creating intermediate objects as needed.StateAccessorInterfacecontract. Two methods:get<T>(target, path): T | nullandset(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.accessoroption. Pass aStateAccessorInterfaceimplementation asaccessor:in theDagonizerconstructor. The dispatcher uses it for all scatter source reads and gather writes in that instance.
Related Concepts
- State Accessors - DottedPathAccessor, StateAccessorInterface contract, and wiring
- Example 04: Scatter Scout - scatter source reads via DottedPathAccessor
- Reference: Contracts - StateAccessorInterface contract