The Data Model Primitives
This page covers the low-level primitives in core/player/src/data/ — plain in-memory storage, a middleware pipeline, and dependency tracking. It does not cover DataController (core/player/src/controllers/data/), which is the higher-level orchestrator built on top of these primitives (formatting, default values, validation wiring, public get/set/delete API). This page is about what DataController composes, not the orchestrator itself.
Key Files
Section titled “Key Files”core/player/src/data/model.ts—DataModelImpl/DataModelMiddlewareinterfaces,PipelinedDataModel,constructModelForPipelinecore/player/src/data/local-model.ts—LocalModelcore/player/src/data/noop-model.ts—NOOPDataModelcore/player/src/data/dependency-tracker.ts—DependencyTracker,DependencyMiddleware,DependencyModel
LocalModel
Section titled “LocalModel”“A data model that stores data in an in-memory JS object” (core/player/src/data/local-model.ts:9). It’s the simplest possible DataModelImpl — get/set/delete/reset operating directly on a plain JS object, using dlv for reads and timm’s setIn/omit/removeAt for immutable writes. This is the base of the pipeline: the actual storage everything else sits in front of.
The Middleware Chain
Section titled “The Middleware Chain”DataModelMiddleware (core/player/src/data/model.ts:74) is the generic shape any pipeline stage implements:
export interface DataModelMiddleware { /** The name of the middleware */ name?: string; set(transaction: BatchSetTransaction, options?: DataModelOptions, next?: DataModelImpl): Updates; get(binding: BindingInstance, options?: DataModelOptions, next?: DataModelImpl): any; delete?(binding: BindingInstance, options?: DataModelOptions, next?: DataModelImpl): void; reset?(): void;}Each middleware receives the next model in the chain and decides whether/how to call it — the same shape used by ValidationMiddleware (which backs ValidationController) and by DependencyMiddleware below. constructModelForPipeline folds an ordered array of these into a single effective DataModelImpl by wrapping each middleware around the next with toModel.
flowchart LR
A[PipelinedDataModel] --> B[DependencyMiddleware]
B --> C[ValidationMiddleware]
C --> D[...other middleware]
D --> E[LocalModel]
PipelinedDataModel
Section titled “PipelinedDataModel”PipelinedDataModel (core/player/src/data/model.ts:222) — “A DataModel that manages middleware data handlers”. It holds a DataPipeline (an ordered array of DataModelMiddleware | DataModelImpl, terminating in something like LocalModel), builds the effective chained model via constructModelForPipeline, and exposes setMiddleware/addMiddleware to change the pipeline at runtime. It has one hook:
| Hook | Type | Fires when |
|---|---|---|
hooks.onSet | SyncHook<[BatchSetTransaction]> | After a set() transaction has been applied through the full pipeline |
NOOPDataModel
Section titled “NOOPDataModel”NOOPDataModel (core/player/src/data/noop-model.ts:7) — “A model that does nothing”, used as a safe default (NOOP_MODEL singleton) when a pipeline is empty, and useful in tests.
DependencyTracker / DependencyMiddleware / DependencyModel
Section titled “DependencyTracker / DependencyMiddleware / DependencyModel”core/player/src/data/dependency-tracker.ts defines a DependencyTracker base class that records every binding read (readDeps) and written (writeDeps) during an operation, with support for named sub-sets ("core" vs "children") so a caller can distinguish “this node’s own reads” from “reads made while resolving its children.”
Two things build on it:
DependencyMiddleware— aDataModelMiddlewarethat records dependencies as a pipeline stage while still forwarding tonext.DependencyModel— a standaloneDataModelImplwrapper around a root model; everyget/set/deleterecords a dependency and then delegates to the wrapped model.
This tracker is not exclusive to DataController. The single most important cross-subsystem fact here: the view Resolver uses DependencyModel directly to implement incremental re-resolution. In core/player/src/view/resolver/index.ts, every node resolution wraps the data model in a fresh DependencyModel (new DependencyModel(options.data.model)), resolves the node’s value against it, and stores the resulting read-dependency set alongside the node’s previously-resolved value. On the next update, the Resolver compares the incoming changed-bindings set against each node’s stored dependency set (caresAboutDataChanges) — if none of that node’s tracked bindings changed, it reuses the cached resolved subtree instead of recomputing it. This is the mechanism behind the “incremental resolve” step described in What Happens When Data Changes.
Relationship to Other Subsystems
Section titled “Relationship to Other Subsystems”- DataController — composes
LocalModel+ a middleware pipeline into aPipelinedDataModel, and adds formatting/defaults/public API on top. - Resolver / Parser — uses
DependencyModelper-node to decide whether to reuse a cached resolved subtree or recompute it. - ValidationController — its
ValidationMiddlewareimplements the sameDataModelMiddlewareinterface described above and sits in the same pipeline.