Deep Object Merging
Relevant source files
The following files were used as context for generating this wiki page:
Deep Object Merging is the core mechanism in keyfold used to fold partial deltas into a nested state tree. Unlike standard deep merge implementations, it distinguishes between plain objects, which it merges recursively, and other types (like unkeyed arrays or class instances), which it replaces wholesale. This process is immutable, ensuring that the original base state is never mutated and that structural sharing is preserved for untouched subtrees.
Sources: README.md:1-20, src/merge.ts:14-25
Core Logic and Architecture
The merging logic is centered around the fold function, which determines how to apply a delta to a base value based on the type of data and the compiled path policy. The architecture prioritizes delta-proportional work, meaning branches not mentioned in the delta are not visited.
The Fold Decision Tree
When a merge is initiated via createMerger, the system follows a specific decision flow for each node in the state tree:
The logic ensures that replace policies act as boundaries where traversal stops immediately.
Sources: src/merge.ts:40-66, AGENTS.md:18-25
Object Merging Semantics
Plain objects are merged by iterating over the own enumerable keys of the delta. The system ignores unsafe keys like __proto__, constructor, and prototype to prevent prototype pollution.
| Feature | Behavior |
|---|---|
| Partial Updates | Unmentioned fields in the base are preserved in the result. |
| Structural Sharing | If a merge results in no changes, the original base reference is returned. |
| Immutability | A shallow copy of the object is only created upon the first detected change. |
| Deleted Fields | Fields are removed if the delta value is the DELETE symbol (or DELETE_TOKEN). |
Sources: src/merge.ts:79-115, src/objects.ts:1-25
Keyed List Reconciliation
When an array path is configured with a keyBy policy, the merger switches from wholesale replacement to identity-based reconciliation. This allows updating specific items within a list without affecting others or losing their order.
Reconciliation Flow
The reconciliation process involves indexing the delta items by their identity field and then iterating through the base list to apply updates.
Sources: src/reconcile.ts:18-56
Configuration and Policy Compilation
Merging behavior is governed by MergeOptions, which are compiled once into a PolicyNode tree. This tree is traversed alongside the data during the merge process.
Configuration Options
| Option | Type | Description |
|---|---|---|
keyBy | Record<string, string> | Maps dot-separated paths to the field used for item identity. |
replace | string[] | Specifies paths where the merge should stop and replace wholesale. |
wireDeletes | boolean | Enables interpretation of DELETE_TOKEN ("@@keyfold/delete") as a delete operation. |
Sources: src/types.ts:3-18, src/paths.ts:25-50
Policy Constraints
- Unreachable Policies: A
replacepath makes any policy nested below it unreachable, which triggers aKeyfoldConfigError. - Identity Restrictions: Identity fields cannot be
$delete,__proto__,constructor, orprototype. - Item Swap Idiom: Using a trailing
[]in areplacepath (e.g.,items[]) allows replacing individual items in a keyed list while still reconciling the list structure itself.
Sources: src/paths.ts:55-85, README.md:60-75
Summary
Deep Object Merging in keyfold provides a predictable way to apply partial updates to complex state trees. By strictly distinguishing between plain objects and atomic values, and by providing a robust mechanism for keyed list reconciliation, it maintains data integrity and performance. The system's adherence to immutability and structural sharing makes it suitable for high-frequency state updates in environments like websocket-driven UIs or complex form management.
Sources: README.md:140-160, AGENTS.md:35-45