Wholesale Replacement
Relevant source files
The following files were used as context for generating this wiki page:
Wholesale replacement is a configuration policy in keyfold that allows specific state paths to be swapped entirely rather than deep-merged or reconciled. While the default behavior for objects is a deep merge and for keyed lists is reconciliation, certain data structures—such as unkeyed arrays, scalars, or complex third-party objects—require an "all-or-nothing" replacement to maintain integrity and performance.
This mechanism acts as a recursion boundary. When the merger encounters a path marked for replacement, it performs a direct pointer swap (if applicable) or a verbatim assignment, bypassing any further inspection of the delta's contents. This is particularly useful for configuration objects or large opaque blobs where structural merging would be semantically incorrect or computationally expensive.
Sources: README.md:38-45, src/merge.ts:37-38, AGENTS.md:23-26
Architecture and Logic
The replacement logic is integrated into the core fold function and the path compilation phase. During compilation, paths specified in the replace array are converted into a PolicyNode tree. Each node in this tree can be flagged with a replace boolean.
The Replacement Boundary
At runtime, the fold function checks the policy for the current path. If replace === true is found on the PolicyNode, the function immediately returns the delta value. This effectively terminates the recursive traversal for that branch.
The diagram shows how the replace policy acts as a short-circuit before any structural analysis occurs.
Sources: src/merge.ts:37-38, src/paths.ts:14-18, README.md:129-134
Path Configuration and Validation
Wholesale replacement is configured via the replace property in MergeOptions. Paths are dot-separated strings representing the structure of the state object.
Configuration Rules
- Verbatim Swap: Paths in the
replacelist are swapped wholesale. - Dead Config Detection: The compiler rejects any
keyByorreplacepolicy nested below an existingreplacepath, as the boundary makes them unreachable. - No Traversal: Replaced values are taken as-is;
keyfolddoes not scan or sanitize for operators (likeDELETEor$delete) inside a replacement value.
| Configuration Element | Type | Description |
|---|---|---|
replace | string[] | Array of path strings (e.g., ["order.shipping"]) to be swapped wholesale. |
[] Suffix | Path segment | Used to indicate a "per-item" replacement within a keyed list. |
Sources: src/paths.ts:49-74, src/types.ts:14-18, README.md:73-77, test/config.test.ts:38-46
The Item-Swap Idiom
A special syntax, path[], is used for keyed lists. While the list itself is reconciled (matching items by identity), each matched item is replaced wholesale by the incoming value instead of being deep-merged. This is known as the item-swap idiom.
Sources: README.md:83-88, src/paths.ts:60-64
Implicit vs. Explicit Replacement
Replacement occurs in two scenarios: explicitly via configuration and implicitly based on data types.
Implicit Replacement
keyfold automatically treats certain types as atomic (opaque). These are replaced wholesale without needing a replace entry in options:
- Scalars: Strings, numbers, booleans, null.
- Unkeyed Arrays: Standard arrays where no
keyByidentity is configured. - Non-Plain Objects: Class instances,
Date,Map,Set,RegExp, andPromise.
Explicit Replacement
Explicit replacement is enforced by the user to override default deep-merge behavior for plain objects.
The diagram illustrates the different triggers that lead to the same wholesale swap behavior.
Sources: README.md:129-134, src/merge.ts:40-62, src/objects.ts:11-18, test/merge.test.ts:41-48
Technical Implementation Details
The implementation ensures that replacement is "cheap" by avoiding object creation unless necessary.
Pointer Swapping
If a merge result matches the base value, the base reference is returned. However, for paths under replace, the delta reference is used directly. This means an empty object {} provided in a delta for a replaced path will result in that exact empty object reference in the new state.
Code Snippet: The Fold Decision
function fold(
base: unknown,
delta: unknown,
policy: PolicyNode | undefined,
context: FoldContext,
path: string,
mode: FoldMode,
): unknown {
// Replace is deliberately a pointer swap: no traversal or sanitizing pass.
if (policy?.replace === true) return delta;
// ... rest of structural merging logic
}
Sources: src/merge.ts:37-38, test/merge.test.ts:98-105
Impact on Deletes and Operators
Inside a wholesale replacement boundary, keyfold operators lose their special meaning and are treated as literal data:
DELETEToken: If theDELETEsymbol appears inside a replaced object, it is stored as the symbol itself, not used to remove a field.$deleteTombstone: In an item-swap scenario (replace: ["list[]"]), a$delete: truefield inside the item is ignored as an operator and kept as data.
The only exception is the field delete at the replacement path itself. If profile.address is configured for replacement, sending { address: DELETE } will successfully remove the address field from the parent object.
Sources: README.md:154-158, test/merge.test.ts:117-123, test/reconcile.test.ts:133-140
Conclusion
Wholesale replacement provides a vital escape hatch from recursive merging. By establishing clear boundaries at specified paths, it ensures that complex or unkeyed data structures are handled predictably and efficiently. It enforces the "last-delta-wins" philosophy for designated subtrees, maintaining performance by bypassing the overhead of structural comparison.
Sources: README.md:175-180, AGENTS.md:23-26