Constraints
Constraints are always-on bounds for numeric state values. After every mutation, Pulse clamps constrained values to their declared range before the view re-renders. The value can never go out of range — there is no code path where it can.
Declaring constraints
The constraints field maps top-level state keys to bounds objects with optional min and max properties:
export default {
route: '/cart',
state: {
quantity: 1,
zoom: 1.0,
rating: 0,
},
constraints: {
quantity: { min: 1, max: 99 },
zoom: { min: 0.5, max: 3.0 },
rating: { min: 0, max: 5 },
},
mutations: {
increaseQty: (state) => ({ quantity: state.quantity + 1 }),
decreaseQty: (state) => ({ quantity: state.quantity - 1 }),
zoomIn: (state) => ({ zoom: state.zoom + 0.1 }),
zoomOut: (state) => ({ zoom: state.zoom - 0.1 }),
},
}
When decreaseQty runs and quantity is already 1, the constraint clamps it back to 1 before the view renders. The mutation does not need to check bounds — the spec declares them once and Pulse enforces them everywhere.
Constraints vs Validation
| Constraints | Validation | |
|---|---|---|
| When it runs | After every mutation, automatically | Only when an action has validate: true |
| What it does | Clamps numeric values silently | Rejects the action and surfaces errors |
| User feedback | None — state is silently corrected | Explicit error messages shown in the view |
| Best for | Numeric ranges that must never be exceeded | Form field correctness before submission |
One-sided bounds
Either min or max can be declared alone — both are optional:
constraints: {
count: { min: 0 }, // no upper limit
discount: { max: 100 }, // no lower limit
offset: { min: 0, max: 999 } // both bounds
}
How clamping works
After Pulse applies a mutation's partial state update, it iterates over all declared constraints and applies Math.max(min, Math.min(max, value)) to each constrained key. The view is then called with the clamped state.
// state.count = 10, constraints.count = { min: 0, max: 10 }
mutations: {
increment: (state) => ({ count: state.count + 1 }),
// mutation returns { count: 11 }
// constraint clamps to 10
// view receives { count: 10 }
}
Top-level keys only
Constraints apply to top-level state keys. To constrain nested values, consider flattening your state structure or applying bounds logic in the mutation itself:
// Cannot do:
constraints: {
'player.health': { min: 0, max: 100 } // ✗ nested paths not supported
}
// Do instead:
state: { playerHealth: 100 },
constraints: { playerHealth: { min: 0, max: 100 } },
// Or handle in the mutation:
mutations: {
takeDamage: (state, _, amount) => ({
player: {
...state.player,
health: Math.max(0, state.player.health - amount),
}
})
}