Skip to content

DataController

The orchestrator for player data.

DataController is the main runtime API that every other subsystem — expressions, the view resolver, plugins — goes through to read or write data. It doesn’t store data itself; it wraps a pipelined model, layers hooks around every operation, and composes whatever middleware (validation, error-handling) was supplied when it was constructed.

  • core/player/src/controllers/data/controller.tsDataController class
  • core/player/src/controllers/data/utils.tsReadOnlyDataController
  • core/player/src/data/model.tsPipelinedDataModel, LocalModel, DataModelMiddleware, DataPipeline types it builds on
constructor(
model: Record<any, unknown> | undefined,
options: {
pathResolver: BindingParser;
middleware?: Array<DataModelMiddleware>;
logger?: Logger;
},
)

DataController is created once per player.start(flow) call, seeded with the flow’s data block and a BindingParser. See Start to Render for where it fits in the overall startup sequence. In practice, Player constructs it with the ValidationController’s middleware (validationController.getDataMiddleware()) and an error-controller middleware already in the middleware array, so validation and error handling are wired in before the first read or write happens.

Internally, DataController lazily builds a PipelinedDataModel the first time getModel() is called, made up of a base LocalModel (the raw in-memory store) plus whatever middleware was supplied — see Data Model for how that pipeline itself works. DataController’s job is everything layered on top of that pipeline:

  • Parsing raw string/array bindings into BindingInstances via the pathResolver before every operation.
  • Diffing old vs. new values (via dequal) so no-op writes don’t fire update hooks.
  • Resolving default values when a get returns undefined.
  • Formatting/deformatting values around the pipeline.
  • Firing onSet/onGet/onUpdate/onDelete hooks so other subsystems (notably the ViewController) can react.

set/get accept a formatted option: setting with formatted: true runs the value through hooks.deformat before it hits the pipeline; getting with formatted: true runs the stored value through hooks.format on the way out (and formatted: false explicitly deformats a value that’s already in the model). Player itself taps both hooks once at startup to delegate to the SchemaController’s registered formatters:

dataController.hooks.format.tap("player", (value, binding) => {
const formatter = schema.getFormatter(binding);
return formatter ? formatter.format(value) : value;
});

See Data Formatting and Deformatting for the full authoring-level explanation of format references and custom format types.

When get resolves to undefined (and the caller didn’t pass ignoreDefaultValue), DataController calls the resolveDefaultValue hook, passing the resolved BindingInstance. Player taps this once at startup to fall back to the type’s default from the schema:

dataController.hooks.resolveDefaultValue.tap(
"player",
(binding) => schema.getApparentType(binding)?.default,
);

serialize() returns hooks.serialize.call(this.get("")) — it reads the entire model from the root binding and runs the result through the serialize waterfall hook before returning it. This gives plugins a chance to redact or reshape the output (for example, the data-filter plugin taps this hook to strip particular paths out of the serialized model).

All of the following are tapable hooks defined on dataController.hooks (verified against controller.ts); nothing in this list is a plain method:

HookTypeGives youUse it when…
resolveSyncWaterfallHookDeclared on the class, but not called or tapped anywhere in core/player today — note that ExpressionEvaluator and the view Resolver each have their own, unrelated hooks.resolve of the same name; don’t confuse the twoProbably not — treat as reserved/unused for now
resolveDataStagesSyncWaterfallHook<[DataPipeline]>The middleware array before it’s frozen into the pipelineYou need to insert/remove/reorder middleware (see the plugin implementation guide for a worked example)
resolveDefaultValueSyncBailHook<[BindingInstance], any>A chance to supply a default when a get resolves to undefinedYou want schema- or convention-based defaults
onDeleteSyncHook<[BindingInstance]>Notification after a delete completesYou need to react to removed bindings
onSetSyncHook<[BatchSetTransaction]>The raw, normalized transaction passed to setYou want to observe every write attempt, including no-ops
onGetSyncHook<[any, any]>The binding and resolved value for every getYou want to observe reads
onUpdateSyncHook<[Updates, DataModelOptions | undefined]>The actual set of changed bindings (no-ops filtered out)You want to react only to real changes — this is what ViewController taps to trigger re-render
formatSyncWaterfallHook<[any, BindingInstance]>A chance to convert a stored value into its user-facing representationYou’re wiring up formatters (see Data Formatting and Deformatting)
deformatSyncWaterfallHook<[any, BindingInstance]>The inverse of formatConverting a user-facing value back to its stored representation
serializeSyncWaterfallHook<[any]>The full model snapshot before serialize() returns itYou want to redact or reshape exported data

makeReadOnly() returns a ReadOnlyDataController (core/player/src/controllers/data/utils.ts) — a wrapper for the Data Controller Class that prevents writes. It implements only get, delegating straight to the underlying controller. player.hooks.state exposes exactly this read-only variant once a flow reaches the completed state, so plugins can inspect final data without being able to mutate it.

  • Data Model — the PipelinedDataModel/LocalModel/middleware machinery DataController wraps
  • ValidationController — supplies its data middleware via getDataMiddleware(), which is injected into DataController’s constructor
  • SchemaController — supplies the formatters used by the format/deformat hooks and the default values used by resolveDefaultValue
  • Data Formatting and Deformatting — full guide on formatter references and custom format types
  • Plugin Implementation — a worked example tapping dataController.hooks.resolveDataStages to conditionally insert middleware