@blumintinc/microdiff
v1.1.0
Published
Small, fast, zero-dependency deep diff with pluggable atomic-type recognition. Fork of AsyncBanana/microdiff.
Downloads
720
Readme
@blumintinc/microdiff
Small, fast, zero-dependency deep diff with pluggable atomic-type recognition. Fork of AsyncBanana/microdiff at v1.5.0.
What this fork adds
Two options on the diff() API let callers configure which values are treated as atomic leaves (compared by value, never recursed into):
interface MicrodiffOptions {
cyclesFix: boolean;
isAtomic: (value: object) => boolean;
isEqualAtomic: (a: unknown, b: unknown) => boolean;
}isAtomic(value)— returntrueto treatvalueas a leaf.isEqualAtomic(a, b)— equality test for two atomic values.Object.isreference/NaN equality is checked separately bydiff(), so this only handles value-equal-but-distinct-reference cases.
When omitted, both options fall back to the published defaults (defaultIsAtomic, defaultEqualAtomic). Defaults treat the following as atomic:
Date,RegExp,String,Number(upstream microdiff parity)Temporal.*family (upstream microdiff parity)- Firestore Timestamp shapes (real
firebase-admin/firestoreinstances and plain{_seconds, _nanoseconds}maps left bystructuredClone/deepmerge) — added in1.1.0
Firestore Timestamp recognition is structural (no firebase-admin dependency) and on by default because BluMint's primary consumer is Firestore-based. Override isAtomic (return false) to disable.
Why this fork exists
Upstream microdiff hardcodes its atomic-type list at module scope. There is no extension hook. For our use case (Firestore Timestamp instances embedded in documents), upstream descends into the Timestamp's _seconds / _nanoseconds private fields and emits per-property CHANGE diffs — which materialize as malformed Firestore writes. The fork exposes the existing recursion gate as a caller-provided predicate so consumers can teach diff() about their own atomic types — and additionally builds Firestore Timestamp recognition into the defaults.
Calls with no options are a strict superset of upstream: identical for everything upstream handles, plus correct treatment of Firestore Timestamps.
Install
npm install @blumintinc/microdiffUsage
Firestore Timestamps are atomic by default (no options needed):
import diff from "@blumintinc/microdiff";
import { Timestamp } from "firebase-admin/firestore";
const before = { lastUpdated: Timestamp.fromMillis(1000) };
const after = { lastUpdated: Timestamp.fromMillis(2000) };
diff(before, after);
// → [{ type: "CHANGE", path: ["lastUpdated"], value: <Timestamp 2000>, oldValue: <Timestamp 1000> }]
// No `_seconds`/`_nanoseconds` descent. Timestamp identity preserved in `value` and `oldValue`.Treat additional custom classes as atomic by extending defaultIsAtomic:
import diff, { defaultIsAtomic, defaultEqualAtomic } from "@blumintinc/microdiff";
import { GeoPoint } from "firebase-admin/firestore";
const isGeoPoint = (v: object) => v instanceof GeoPoint;
diff(before, after, {
isAtomic: (v) => isGeoPoint(v) || defaultIsAtomic(v),
isEqualAtomic: (a, b) => {
if (isGeoPoint(a as object) && isGeoPoint(b as object)) {
return (a as GeoPoint).isEqual(b as GeoPoint);
}
return defaultEqualAtomic(a, b);
},
});Note the composition pattern: extend defaultIsAtomic instead of replacing it, otherwise Date / RegExp / Temporal.* / Firestore Timestamps lose their atomic status.
API
| Export | Description |
|---|---|
| diff (default) | The deep-diff function. Drop-in API-compatible with upstream microdiff. |
| defaultIsAtomic | Default predicate. Matches Date, RegExp, String, Number, Temporal.*, and Firestore Timestamp shapes. |
| defaultEqualAtomic | Default equality. Timestamps compare via defaultTimestampToMillis; Temporal via String(); rich types via NaN-aware numeric/string coerce. |
| defaultIsTimestampShape | Structural duck-type predicate for Firestore Timestamps (real instances + plain-map shapes). |
| defaultTimestampToMillis | Extracts epoch ms from any Firestore Timestamp shape. |
| defaultRichTypes | Frozen readonly string[] of the four rich-type constructor names. |
| defaultTemporalTypes | Frozen readonly string[] of the Temporal API constructor names available on globalThis.Temporal. |
| Difference<TData> | Discriminated union of DifferenceCreate / DifferenceRemove / DifferenceChange. Generic over TData for typed paths and values. |
| MicrodiffOptions | Options shape. All fields optional via Partial<MicrodiffOptions> at the call site. |
License
MIT, preserved from upstream. See LICENSE for the full text including the BluMint modification notice.
