State Accessors
What It Is
State Accessors explains the tiny but important contract that decides how Dagonizer reads and writes state paths. DottedPathAccessor is the default: it walks dotted paths like customer.address.city, returns undefined for missing reads, and auto-vivifies intermediate objects on writes.
Use this page when plain JavaScript dotted paths are not enough. Custom accessors let applications keep the DAG contract stable while swapping the state substrate underneath it: namespaced objects, proxy-backed stores, domain-specific path syntax, guarded writes, or any other state model that still needs scatter, embedded DAGs, and gather to agree on the same path semantics.
How It Works
The dispatcher delegates path reads and writes to the configured accessor. Scatter source, embedded-DAG stateMapping, and gather target paths all flow through the same two methods: get(state, path) and set(state, path, value).
Scatter source reads, scatter state-mapping input copies, and gather writes all walk paths into the live state object. The StateAccessor contract defines that walk; DottedPathAccessor is the default implementation.
Diagrams, Examples, and Outputs
State accessors are runtime path semantics, not graph topology, so this page uses focused source snippets rather than a new diagram. The same accessor governs every placement field that names a state path.
- Subclassing State - the state object the accessor reads from and writes to
- DAGBuilder - placements that use
sourceandtargetpaths run through the accessor - State Accessor - runnable custom accessor and gather-strategy example
What It Lets You Do
Use when
Use a custom state accessor when dotted JavaScript paths are not the right way to read scatter sources, state mappings, or gather targets. This applies to namespaced state, proxy-backed state, alternate path syntaxes, or controlled write policies.
Code Samples
API surface
| Symbol | Source | Role |
|---|---|---|
StateAccessor | @studnicky/dagonizer/contracts | The contract, get(state, path) and set(state, path, value) |
DottedPathAccessor | @studnicky/dagonizer/runtime | Default impl: path.split('.') walks, writes auto-vivify intermediate objects |
DagonizerOptionsType.accessor | @studnicky/dagonizer | Constructor slot for a custom accessor |
The contract
// StateAccessorInterface: get(target, path) → T | null; set(target, path, value) → void.
// Implementations are stateless; the same instance resolves every scatter source
// read, state-mapping input copy, and gather write.
export const dotAccessor: StateAccessorInterface = new DottedPathAccessor();Implementations are stateless. The same instance is shared across every scatter source read, state-mapping input copy, and gather write.
Details for Nerds
Default behavior
DottedPathAccessor ships in @studnicky/dagonizer/runtime:
// Read a nested value by dotted path; returns `null` on a miss.
export const accessor = new DottedPathAccessor();// Write a nested value by dotted path; intermediate objects are auto-vivified.
export const writeAccessor = new DottedPathAccessor();Nested writes auto-vivify intermediate objects. Reads through a missing or non-object segment return undefined.
Swapping in a custom accessor
Pass it via the dispatcher constructor:
/**
* PrefixAccessor: a custom StateAccessorInterface that silently adds a fixed namespace
* prefix to every key before delegating to DottedPathAccessor.
* Demonstrates the adapter contract: implement get + set, no callbacks.
*/
export class PrefixAccessor implements StateAccessorInterface {
readonly #prefix: string;
readonly #inner: DottedPathAccessor;
constructor(prefix: string) {
this.#prefix = prefix;
this.#inner = new DottedPathAccessor();
}
get(target: object, path: string): unknown {
return this.#inner.get(target, `${this.#prefix}.${path}`);
}
set(target: object, path: string, value: unknown): void {
this.#inner.set(target, `${this.#prefix}.${path}`, value);
}
}// Pass any StateAccessorInterface to the Dagonizer constructor; scatter source reads
// and gather writes will use it for every execution.
export const prefixedAccessor = new PrefixAccessor('archivist');The same accessor flows through every code path that resolves a state path:
scatter.source: reading the array to scatter over.scatter.stateMapping.input(builder optioninputs): copying parent fields into each clone before the body runs.EmbeddedDAGNode.stateMapping.input/stateMapping.output: seeding the child-state clone and copying fields back after the sub-DAG completes.gather.mapping(map strategy): writing produced clone fields back to parent paths.gather.target(append strategy): writing the gathered results.gather.partitions(partition strategy): writing each output bucket.
Accessor inside gather strategies
Custom GatherStrategy subclasses receive the dispatcher's accessor on the execution context:
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());Every state-path read and write goes through one resolution strategy.
Related Concepts
- Subclassing State - the state object the accessor reads from and writes to
- DAGBuilder - placements that use
sourceandtargetpaths run through the accessor - State Accessor - runnable custom accessor example
- Reference: Contracts
- Reference: Runtime
- Reference: Core