A delayed State transition should still mean the same thing when it finally continues. Chapter 10 introduces the Delay Controller as a timing boundary: every pipeline attempt pauses for a configured interval, then continues with the same candidate State. The service owns withDelay() configuration, while the component exposes the elapsed teaching experience without taking ownership of the pipeline through the FeatureCell boundary.
A pause can look like a debounce, a throttle, or a hidden mutation rule when the only visible evidence is that a button feels slower. The Chapter 10 lab makes the boundary explicit: the Delay Controller changes when an accepted update proceeds, not which update is processed or what its value means.
Key takeaway: Begin with the completed Chapter 7 project. Chapter 10 adds a fixed three-second interval and a display-only elapsed timer to the existing character workflow.
Delay Changes Time, Not Meaning
A FeatureCell update still starts with a candidate value. The service submits that candidate through the same service-owned boundary, and the configured pipeline still determines whether it can become committed State. The new behavior is the pause before the pipeline attempt proceeds through its configured timing policy.
That makes Delay useful when timing is part of the user experience but not part of the data rule. You might want a visible animation to settle before a panel changes, a demo to represent realistic latency, or an external handoff to receive a predictable interval. In each case, the candidate remains the candidate. Delay does not rewrite the object, select a different record, or turn a state update into a new kind of update.
The service decides that this FeatureCell uses a fixed delay, the controller applies that timing policy, and the component renders the result. The component does not create a second timer to decide when State is allowed to change.
Registering the Delay Controller
The Delay Controller must be registered before the service can configure it. In the Angular tutorial, that registration belongs beside the FeatureCell provider in app.config.ts. The application supplies the controller as infrastructure; the service supplies the interval as feature policy.
The source example registers the controller alongside the existing array merge behavior:
import { withArrayByIdMergeBehavior, withDelayController } from '@sdux-vault/addons';
provideFeatureCell(
ExampleService,
{
key: 'star-wars-character',
initialState: STAR_WARS_CHARACTERS
},
[withArrayByIdMergeBehavior],
[withDelayController]
);
Registration makes the controller available at the FeatureCell boundary. It does not tell the feature how long to wait. That decision comes next, in the service that already owns the character State and its update methods.
Configure a Fixed Interval in the Service
Chapter 10 keeps timing configuration next to the other FeatureCell pipeline configuration. The service declares the interval as a named constant and calls withDelay() before initialization. This keeps the policy discoverable and prevents individual component actions from inventing their own timing rules.
/** Fixed Policy-stage hold applied to every tutorial pipeline attempt. */
export const EXAMPLE_DELAY_MILLISECONDS = 3_000;
constructor() {
this.#vault.withDelay?.({
millisecondDelay: EXAMPLE_DELAY_MILLISECONDS
});
this.#vault.initialize();
}
The fixed interval is not a component setting. A create, edit, or delete method submitted through this service encounters the same configured timing policy. The feature does not become fast for one button and delayed for another because each handler implemented its own waiting logic.
⚠️ Warning: Register the controller at the provider boundary and configure
withDelay()before callinginitialize(). The service should be the one place where this FeatureCell's pipeline policy is assembled.
Make the Pause Observable Without Moving Pipeline Authority
A three-second pause is difficult to understand if the screen only appears unresponsive. The tutorial therefore adds a small elapsed timer to the component. It starts when the service reports that an update is loading and stops when that update reaches its normal completion, finalization, or error presentation path.
The timer explains what the learner is seeing; it does not control the update. It does not call setTimeout() around a service method, decide whether a second click should be ignored, or delay a component-local copy of State. The FeatureCell remains the source of loading and State information, and the service remains the owner of the update boundary.
The configured interval can be rendered beside the live elapsed value, so the learner can observe that the pause is deliberate. Removing the display helper removes the lesson's stopwatch, but it does not remove the Delay policy from the service.
| Concern | Owner | Responsibility |
|---|---|---|
| Timing policy | Service and Delay Controller | Configure and apply the fixed interval for the FeatureCell pipeline. |
| Candidate State | FeatureCell pipeline | Continue the submitted candidate without changing its meaning. |
| Elapsed display | Component | Show the teaching experience without deciding when State may commit. |
Every Attempt Gets the Configured Interval
Delay is easiest to reason about when its unit is an attempt. A user action submits a candidate, and that attempt observes the configured interval before it continues. The rule is not “wait until the user stops clicking” and it is not “run at most once per window.” It is simply a fixed pause associated with each pipeline attempt.
If two actions are initiated close together, each one has its own timing experience. Delay does not secretly collapse them into one update or claim that the latest action is automatically the only meaningful one. The rest of the configured FeatureCell pipeline still determines how those candidates are processed and committed.
The Chapter 10 screen makes this visible with repeated updates. Watch the loading and elapsed values, then compare the committed character State. The delay changes when you see the result; it does not add a second business rule about which character update should win.
Why Delay Is Not Debounce or Throttle
Debounce and throttle answer questions about the relationship among multiple incoming events. Debounce waits for quiet and typically keeps the latest event. Throttle limits how often work is allowed to start within a window. Those are event-admission rules.
Delay answers a different question: once this pipeline attempt is accepted, how long should it pause before continuing? It does not change the candidate, suppress a candidate because another event arrived, or redefine the update as “latest wins.” Calling every visible pause a debounce makes the timing policy sound more powerful than it is and encourages ownership in the wrong layer.
Key takeaway: Use Delay for a fixed execution interval. Choose debounce or throttle only when the requirement is explicitly about admitting, grouping, or limiting a sequence of events. Keep the candidate State and the timing rule conceptually separate.
The Chapter 10 Boundary
The completed lab has three clear owners. The application registers withDelayController. The service configures withDelay() and keeps the FeatureCell update methods together. The component renders loading, elapsed time, and committed State as a teaching surface.
That arrangement gives timing a place without letting timing become a second state architecture. You can change three seconds to a different fixed interval, remove the elapsed display, or use the same policy with another feature while preserving the underlying meaning of the State transition.
The next time an update needs to wait, ask whether the requirement is about when an accepted attempt proceeds or which events should be admitted. If it is the former, Delay can express that intent directly. Keep configuration with the service, registration with FeatureCell infrastructure, and explanation with the component.
Try the complete Chapter 10 example in StackBlitz, then compare its setup with the Delay Controller reference and the FeatureCell API.
Top comments (0)