Choose between snapshot and vault
The problem
You have sensitive data — an auth token object, a user profile, a license payload — and need it to be immutable. Both snapshot and vault produce a deep-frozen, structurally independent copy. The APIs look similar and both prevent callers from mutating the data, so it is not obvious which to reach for.
Requirements
- Writes to the returned value must throw.
- The stored data must be independent of the original — mutations to the source after wrapping must not affect the copy.
- Choice may also depend on: whether callers can hold a stable reference, and whether the value is read once or repeatedly.
Approach
The key distinction is reference escape and per-access cost:
snapshotclones once and returns a single frozen object. Every caller that receives the result holds the same frozen reference. One clone, zero marginal cost per subsequent read.vaultnever hands out a stable reference. Every.get()call returns a fresh frozen clone. A caller who retains one result cannot influence the next read from the vault. Cost is O(n) per.get()call (U2).
Use this matrix to decide:
| Scenario | Recommendation |
|---|---|
| Reference escape is acceptable | snapshot |
| Reference escape must be impossible | vault |
| Hot path, single read | snapshot |
| Per-request isolation needed | vault |
| Callers may cache the reference | snapshot is fine |
| Untrusted code receives the result | vault (fresh copy each time) |
Implementation
ts
import { snapshot, vault } from 'constancy';
const raw = { token: 'secret-abc', role: 'admin', expiresAt: 1900000000 };
// --- snapshot: one clone, stable reference ---
const snapped = snapshot(raw);
snapped.role = 'hack'; // TypeError — frozen
// All callers share the same frozen object:
const ref1 = snapped;
const ref2 = snapped;
console.log(ref1 === ref2); // true — same reference
// --- vault: fresh copy per .get(), no stable reference ---
const v = vault(raw);
const copy1 = v.get();
const copy2 = v.get();
console.log(copy1 === copy2); // false — new frozen copy each time
copy1.role = 'hack'; // TypeError — frozen
v.get().role; // 'admin' — vault is pristineTradeoffs
vault.get()is O(n) on every call — full deep clone + freeze of the stored graph (U2). Do not call it in a tight loop. Cache the result for the duration of a single operation if n is non-trivial.snapshotreturns a stable reference. If you hand it to aWeakMap, pass it as a React prop, or compare it with===, you always get the same object.vaultresults are never===-equal across calls.- Neither API supports non-cloneable values: functions, Symbols, DOM nodes throw
TypeErrorat construction/call time. - Neither API blocks
Map/Setinternal slot mutators. If the stored value contains aMap,.set()still works on the returned copy. Wrap withimmutableMapView/immutableSetViewbefore snapshotting if full collection immutability is required.
Alternatives considered
secureSnapshot— stronger descriptor hardening (getter-only, non-configurable) thansnapshot. Recommended for plain configs whereObject.getOwnPropertyDescriptoraccess is a threat. Rejected here as a general alternative because it throws for any non-plain value (Date, Array, class instances), making it unsuitable for heterogeneous data like auth token objects.immutableView— O(1) wrap, no clone. Does not sever the reference (V8). An attacker or bug that retains the original source can still mutate through it. Not a substitute when reference severance is required.