json-diff-ts 5.0.0-alpha.1 → 5.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,34 +10,108 @@
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
11
11
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
13
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow.svg?logo=buy-me-a-coffee)](https://buymeacoffee.com/leitwolf)
13
14
 
14
- ## Overview
15
+ **Deterministic JSON state transitions with key-based array identity.** A TypeScript JSON diff library that computes, applies, and reverts atomic changes using the [JSON Atom](https://github.com/ltwlf/json-atom-format) wire format -- a JSON Patch alternative with stable array paths, built-in undo/redo for JSON, and language-agnostic state synchronization.
15
16
 
16
- **Modern TypeScript JSON diff library** - `json-diff-ts` is a lightweight, high-performance TypeScript library for calculating and applying differences between JSON objects. Perfect for modern web applications, state management, data synchronization, and real-time collaborative editing.
17
+ Zero dependencies. TypeScript-first. ESM + CommonJS. Trusted by thousands of developers ([500K+ weekly npm downloads](https://www.npmjs.com/package/json-diff-ts)).
17
18
 
18
- ### 🚀 **Why Choose json-diff-ts?**
19
+ ## Why Index-Based Diffing Breaks
19
20
 
20
- - **🔥 Zero dependencies** - Lightweight bundle size
21
- - **⚡ High performance** - Optimized algorithms for fast JSON diffing and patching
22
- - **🎯 95%+ test coverage** - Thoroughly tested with comprehensive test suite
23
- - **📦 Modern ES modules** - Full TypeScript support with tree-shaking
24
- - **🔧 Flexible API** - Compare, diff, patch, and atomic operations
25
- - **🌐 Universal** - Works in browsers, Node.js, and edge environments
26
- - **✅ Production ready** - Used in enterprise applications worldwide
27
- - **🎯 TypeScript-first** - Full type safety and IntelliSense support
28
- - **🔧 Modern features** - ESM + CommonJS, JSONPath, atomic operations
29
- - **📦 Production ready** - Battle-tested with comprehensive test suite
21
+ Most JSON diff libraries track array changes by position. Insert one element at the start and every path shifts:
30
22
 
31
- ### ✨ **Key Features**
23
+ ```text
24
+ Remove /items/0 ← was actually "Widget"
25
+ Add /items/0 ← now it's "NewItem"
26
+ Update /items/1 ← this used to be /items/0
27
+ ...
28
+ ```
29
+
30
+ This makes diffs fragile -- you can't store them, replay them reliably, or build audit logs on top of them. Reorder the array and every operation is wrong. This is the fundamental problem with index-based formats like JSON Patch (RFC 6902): paths like `/items/0` are positional, so any insertion, deletion, or reorder invalidates every subsequent path.
31
+
32
+ **json-diff-ts solves this with key-based identity.** Array elements are matched by a stable key (`id`, `sku`, or any field), and paths use JSONPath filter expressions that survive insertions, deletions, and reordering:
33
+
34
+ ```typescript
35
+ import { diffAtom, applyAtom, revertAtom } from 'json-diff-ts';
36
+
37
+ const before = {
38
+ items: [
39
+ { id: 1, name: 'Widget', price: 9.99 },
40
+ { id: 2, name: 'Gadget', price: 24.99 },
41
+ ],
42
+ };
43
+
44
+ const after = {
45
+ items: [
46
+ { id: 2, name: 'Gadget', price: 24.99 }, // reordered
47
+ { id: 1, name: 'Widget Pro', price: 14.99 }, // renamed + repriced
48
+ { id: 3, name: 'Doohickey', price: 4.99 }, // added
49
+ ],
50
+ };
51
+
52
+ const atom = diffAtom(before, after, { arrayIdentityKeys: { items: 'id' } });
53
+ ```
54
+
55
+ The atom tracks _what_ changed, not _where_ it moved:
56
+
57
+ ```json
58
+ {
59
+ "format": "json-atom",
60
+ "version": 1,
61
+ "operations": [
62
+ { "op": "replace", "path": "$.items[?(@.id==1)].name", "value": "Widget Pro", "oldValue": "Widget" },
63
+ { "op": "replace", "path": "$.items[?(@.id==1)].price", "value": 14.99, "oldValue": 9.99 },
64
+ { "op": "add", "path": "$.items[?(@.id==3)]", "value": { "id": 3, "name": "Doohickey", "price": 4.99 } }
65
+ ]
66
+ }
67
+ ```
68
+
69
+ Apply forward to get the new state, or revert to restore the original:
70
+
71
+ ```typescript
72
+ // Clone before applying — applyAtom mutates the input object
73
+ const updated = applyAtom(structuredClone(before), atom); // updated === after
74
+ const restored = revertAtom(structuredClone(updated), atom); // restored === before
75
+ ```
76
+
77
+ ## Quick Start
78
+
79
+ ```typescript
80
+ import { diffAtom, applyAtom, revertAtom } from 'json-diff-ts';
81
+
82
+ const oldObj = {
83
+ items: [
84
+ { id: 1, name: 'Widget', price: 9.99 },
85
+ { id: 2, name: 'Gadget', price: 24.99 },
86
+ ],
87
+ };
32
88
 
33
- - **Key-based array identification**: Compare array elements using keys instead of indices for more intuitive diffing
34
- - **JSONPath support**: Target specific parts of JSON documents with precision
35
- - **Atomic changesets**: Transform changes into granular, independently applicable operations
36
- - **Dual module support**: Works with both ECMAScript Modules and CommonJS
37
- - **Type change handling**: Flexible options for handling data type changes
38
- - **Path skipping**: Skip nested paths during comparison for performance
89
+ const newObj = {
90
+ items: [
91
+ { id: 1, name: 'Widget Pro', price: 9.99 },
92
+ { id: 2, name: 'Gadget', price: 24.99 },
93
+ { id: 3, name: 'Doohickey', price: 4.99 },
94
+ ],
95
+ };
96
+
97
+ // 1. Compute an atom between two JSON objects
98
+ const atom = diffAtom(oldObj, newObj, {
99
+ arrayIdentityKeys: { items: 'id' }, // match array elements by 'id' field
100
+ });
101
+ // atom.operations =>
102
+ // [
103
+ // { op: 'replace', path: '$.items[?(@.id==1)].name', value: 'Widget Pro', oldValue: 'Widget' },
104
+ // { op: 'add', path: '$.items[?(@.id==3)]', value: { id: 3, name: 'Doohickey', price: 4.99 } }
105
+ // ]
106
+
107
+ // 2. Apply the atom to produce the new state
108
+ const updated = applyAtom(structuredClone(oldObj), atom);
109
+
110
+ // 3. Revert the atom to restore the original state
111
+ const reverted = revertAtom(structuredClone(updated), atom);
112
+ ```
39
113
 
40
- This library is particularly valuable for applications where tracking changes in JSON data is crucial, such as state management systems, form handling, or data synchronization.
114
+ That's it. `atom` is a plain JSON object you can store in a database, send over HTTP, or consume in any language.
41
115
 
42
116
  ## Installation
43
117
 
@@ -45,413 +119,634 @@ This library is particularly valuable for applications where tracking changes in
45
119
  npm install json-diff-ts
46
120
  ```
47
121
 
48
- ## Quick Start
122
+ ```typescript
123
+ // ESM / TypeScript
124
+ import { diffAtom, applyAtom, revertAtom } from 'json-diff-ts';
125
+
126
+ // CommonJS
127
+ const { diffAtom, applyAtom, revertAtom } = require('json-diff-ts');
128
+ ```
129
+
130
+ ## What is JSON Atom?
131
+
132
+ [JSON Atom](https://github.com/ltwlf/json-atom-format) is a specification for representing atomic changes to JSON documents. json-diff-ts is the originating implementation from which the spec was derived.
133
+
134
+ ```text
135
+ json-atom-format (specification)
136
+ ├── json-diff-ts (TypeScript implementation) ← this package
137
+ └── json-atom-py (Python implementation)
138
+ ```
139
+
140
+ The specification defines the wire format. Each language implementation produces and consumes compatible atoms.
141
+
142
+ An atom is a self-describing JSON document you can store, transmit, and consume in any language:
143
+
144
+ - **Three operations** -- `add`, `remove`, `replace`. Nothing else to learn.
145
+ - **JSONPath-based paths** -- `$.items[?(@.id==1)].name` identifies elements by key, not index.
146
+ - **Reversible by default** -- every `replace` and `remove` includes `oldValue` for undo.
147
+ - **Self-identifying** -- the `format` field makes atoms discoverable without external context.
148
+ - **Extension-friendly** -- unknown properties are preserved; `x_`-prefixed properties are future-safe.
149
+
150
+ ### JSON Atom vs JSON Patch (RFC 6902)
151
+
152
+ JSON Patch uses JSON Pointer paths like `/items/0` that reference array elements by index. When an element is inserted at position 0, every subsequent path shifts -- `/items/1` now points to what was `/items/0`. This makes stored patches unreliable for JSON change tracking, audit logs, or undo/redo across time.
153
+
154
+ JSON Atom uses JSONPath filter expressions like `$.items[?(@.id==1)]` that identify elements by a stable key. The path stays valid regardless of insertions, deletions, or reordering.
155
+
156
+ | | JSON Atom | JSON Patch (RFC 6902) |
157
+ | --- | --- | --- |
158
+ | Path syntax | JSONPath (`$.items[?(@.id==1)]`) | JSON Pointer (`/items/0`) |
159
+ | Array identity | Key-based -- survives reorder | Index-based -- breaks on insert/delete |
160
+ | Reversibility | Built-in `oldValue` | Not supported |
161
+ | Self-describing | `format` field in envelope | No envelope |
162
+ | Specification | [json-atom-format](https://github.com/ltwlf/json-atom-format) | [RFC 6902](https://tools.ietf.org/html/rfc6902) |
163
+
164
+ ---
165
+
166
+ ## JSON Atom API
167
+
168
+ ### `diffAtom` -- Compute an Atom
49
169
 
50
170
  ```typescript
51
- import { diff, applyChangeset } from 'json-diff-ts';
52
-
53
- // Two versions of data
54
- const oldData = { name: 'Luke', level: 1, skills: ['piloting'] };
55
- const newData = { name: 'Luke Skywalker', level: 5, skills: ['piloting', 'force'] };
56
-
57
- // Calculate differences
58
- const changes = diff(oldData, newData);
59
- console.log(changes);
60
- // Output: [
61
- // { type: 'UPDATE', key: 'name', value: 'Luke Skywalker', oldValue: 'Luke' },
62
- // { type: 'UPDATE', key: 'level', value: 5, oldValue: 1 },
63
- // { type: 'ADD', key: 'skills', value: 'force', embeddedKey: '1' }
64
- // ]
171
+ const atom = diffAtom(
172
+ { user: { name: 'Alice', role: 'viewer' } },
173
+ { user: { name: 'Alice', role: 'admin' } }
174
+ );
175
+ // atom.operations [{ op: 'replace', path: '$.user.role', value: 'admin', oldValue: 'viewer' }]
176
+ ```
177
+
178
+ #### Keyed Arrays
179
+
180
+ Match array elements by identity key. Filter paths use canonical typed literals per the spec:
65
181
 
66
- // Apply changes to get the new object
67
- const result = applyChangeset(oldData, changes);
68
- console.log(result); // { name: 'Luke Skywalker', level: 5, skills: ['piloting', 'force'] }
182
+ ```typescript
183
+ const atom = diffAtom(
184
+ { users: [{ id: 1, role: 'viewer' }, { id: 2, role: 'editor' }] },
185
+ { users: [{ id: 1, role: 'admin' }, { id: 2, role: 'editor' }] },
186
+ { arrayIdentityKeys: { users: 'id' } }
187
+ );
188
+ // atom.operations → [{ op: 'replace', path: '$.users[?(@.id==1)].role', value: 'admin', oldValue: 'viewer' }]
69
189
  ```
70
190
 
71
- ### Import Options
191
+ #### Non-reversible Mode
192
+
193
+ Omit `oldValue` fields when you don't need undo:
72
194
 
73
- **TypeScript / ES Modules:**
74
195
  ```typescript
75
- import { diff } from 'json-diff-ts';
196
+ const atom = diffAtom(source, target, { reversible: false });
76
197
  ```
77
198
 
78
- **CommonJS:**
79
- ```javascript
80
- const { diff } = require('json-diff-ts');
199
+ ### `applyAtom` -- Apply an Atom
200
+
201
+ Applies operations sequentially. Always use the return value (required for root-level replacements):
202
+
203
+ ```typescript
204
+ const result = applyAtom(structuredClone(source), atom);
81
205
  ```
82
206
 
83
- ## Core Features
207
+ ### `revertAtom` -- Revert an Atom
84
208
 
85
- ### `diff`
209
+ Computes the inverse and applies it. Requires `oldValue` on all `replace` and `remove` operations:
210
+
211
+ ```typescript
212
+ const original = revertAtom(structuredClone(target), atom);
213
+ ```
86
214
 
87
- Generates a difference set for JSON objects. When comparing arrays, if a specific key is provided, differences are determined by matching elements via this key rather than array indices.
215
+ ### `invertAtom` -- Compute the Inverse
88
216
 
89
- #### Basic Example with Star Wars Data
217
+ Returns a new atom that undoes the original (spec Section 9.2):
90
218
 
91
219
  ```typescript
92
- import { diff } from 'json-diff-ts';
220
+ const inverse = invertAtom(atom);
221
+ // add ↔ remove, replace swaps value/oldValue, order reversed
222
+ ```
93
223
 
94
- // State during A New Hope - Desert planet, small rebel cell
95
- const oldData = {
96
- location: 'Tatooine',
97
- mission: 'Rescue Princess',
98
- status: 'In Progress',
99
- characters: [
100
- { id: 'LUKE_SKYWALKER', name: 'Luke Skywalker', role: 'Farm Boy', forceTraining: false },
101
- { id: 'LEIA_ORGANA', name: 'Princess Leia', role: 'Prisoner', forceTraining: false }
102
- ],
103
- equipment: ['Lightsaber', 'Blaster']
104
- };
224
+ ### `validateAtom` -- Validate Structure
105
225
 
106
- // State after successful rescue - Base established, characters evolved
107
- const newData = {
108
- location: 'Yavin Base',
109
- mission: 'Destroy Death Star',
110
- status: 'Complete',
111
- characters: [
112
- { id: 'LUKE_SKYWALKER', name: 'Luke Skywalker', role: 'Pilot', forceTraining: true, rank: 'Commander' },
113
- { id: 'HAN_SOLO', name: 'Han Solo', role: 'Smuggler', forceTraining: false, ship: 'Millennium Falcon' }
114
- ],
115
- equipment: ['Lightsaber', 'Blaster', 'Bowcaster', 'X-wing Fighter']
116
- };
226
+ ```typescript
227
+ const { valid, errors } = validateAtom(maybeAtom);
228
+ ```
117
229
 
118
- const diffs = diff(oldData, newData, { embeddedObjKeys: { characters: 'id' } });
119
- console.log(diffs);
120
- // First operations:
121
- // [
122
- // { type: 'UPDATE', key: 'location', value: 'Yavin Base', oldValue: 'Tatooine' },
123
- // { type: 'UPDATE', key: 'mission', value: 'Destroy Death Star', oldValue: 'Rescue Princess' },
124
- // { type: 'UPDATE', key: 'status', value: 'Complete', oldValue: 'In Progress' },
125
- // ...
230
+ ### API Reference
231
+
232
+ | Function | Signature | Description |
233
+ | --- | --- | --- |
234
+ | `diffAtom` | `(oldObj, newObj, options?) => IJsonAtom` | Compute a canonical JSON Atom |
235
+ | `applyAtom` | `(obj, atom) => any` | Apply an atom sequentially. Returns the result |
236
+ | `revertAtom` | `(obj, atom) => any` | Revert a reversible atom |
237
+ | `invertAtom` | `(atom) => IJsonAtom` | Compute the inverse atom |
238
+ | `validateAtom` | `(atom) => { valid, errors }` | Structural validation |
239
+ | `toAtom` | `(changeset, options?) => IJsonAtom` | Bridge: v4 changeset to JSON Atom |
240
+ | `fromAtom` | `(atom) => IAtomicChange[]` | Bridge: JSON Atom to v4 atomic changes |
241
+ | `squashAtoms` | `(source, atoms, options?) => IJsonAtom` | Compact multiple atoms into one net-effect atom |
242
+ | `atomMap` | `(atom, fn) => IJsonAtom` | Transform each operation in an atom |
243
+ | `atomStamp` | `(atom, extensions) => IJsonAtom` | Set extension properties on all operations |
244
+ | `atomGroupBy` | `(atom, keyFn) => Record<string, IJsonAtom>` | Group operations into sub-atoms |
245
+ | `operationSpecDict` | `(op) => IAtomOperation` | Strip extension properties from operation |
246
+ | `operationExtensions` | `(op) => Record<string, any>` | Get extension properties from operation |
247
+ | `atomSpecDict` | `(atom) => IJsonAtom` | Strip all extensions from atom |
248
+ | `atomExtensions` | `(atom) => Record<string, any>` | Get envelope extensions from atom |
249
+ | `leafProperty` | `(op) => string \| null` | Terminal property name from operation path |
250
+
251
+ ### AtomOptions
252
+
253
+ Extends the base `Options` interface:
254
+
255
+ ```typescript
256
+ interface AtomOptions extends Options {
257
+ reversible?: boolean; // Include oldValue for undo. Default: true
258
+ arrayIdentityKeys?: Record<string, string | FunctionKey>;
259
+ keysToSkip?: readonly string[];
260
+ }
261
+ ```
262
+
263
+ ### Atom Workflow Helpers
264
+
265
+ Transform, inspect, and compact atoms for workflow automation.
266
+
267
+ #### `squashAtoms` -- Compact Multiple Atoms
268
+
269
+ Combine a sequence of atoms into a single net-effect atom. Useful for compacting audit logs or collapsing undo history:
270
+
271
+ ```typescript
272
+ import { diffAtom, applyAtom, squashAtoms } from 'json-diff-ts';
273
+
274
+ const source = { name: 'Alice', role: 'viewer' };
275
+ const d1 = diffAtom(source, { name: 'Bob', role: 'viewer' });
276
+ const d2 = diffAtom({ name: 'Bob', role: 'viewer' }, { name: 'Bob', role: 'admin' });
277
+
278
+ const squashed = squashAtoms(source, [d1, d2]);
279
+ // squashed.operations => [
280
+ // { op: 'replace', path: '$.name', value: 'Bob', oldValue: 'Alice' },
281
+ // { op: 'replace', path: '$.role', value: 'admin', oldValue: 'viewer' }
126
282
  // ]
283
+
284
+ // Verify: applying the squashed atom equals applying both sequentially
285
+ const result = applyAtom(structuredClone(source), squashed);
286
+ // result => { name: 'Bob', role: 'admin' }
127
287
  ```
128
288
 
129
- #### Advanced Options
289
+ Options: `reversible`, `arrayIdentityKeys`, `target` (pre-computed final state), `verifyTarget` (default: true).
130
290
 
131
- ##### Path-based Key Identification
291
+ #### `atomMap` / `atomStamp` / `atomGroupBy` -- Atom Transformations
132
292
 
133
- ```javascript
134
- import { diff } from 'json-diff-ts';
293
+ All transforms are immutable — they return new atoms without modifying the original:
294
+
295
+ ```typescript
296
+ import { diffAtom, atomMap, atomStamp, atomGroupBy } from 'json-diff-ts';
297
+
298
+ const atom = diffAtom(
299
+ { name: 'Alice', age: 30, role: 'viewer' },
300
+ { name: 'Bob', age: 31, status: 'active' }
301
+ );
135
302
 
136
- // Using nested paths for sub-arrays
137
- const diffs = diff(oldData, newData, { embeddedObjKeys: { 'characters.equipment': 'id' } });
303
+ // Stamp metadata onto every operation
304
+ const stamped = atomStamp(atom, { x_author: 'system', x_ts: Date.now() });
138
305
 
139
- // Designating root with '.' - useful for complex nested structures
140
- const diffs = diff(oldData, newData, { embeddedObjKeys: { '.characters.allies': 'id' } });
306
+ // Transform operations
307
+ const prefixed = atomMap(atom, (op) => ({
308
+ ...op,
309
+ path: op.path.replace('$', '$.data'),
310
+ }));
311
+
312
+ // Group by operation type
313
+ const groups = atomGroupBy(atom, (op) => op.op);
314
+ // groups => { replace: IJsonAtom, add: IJsonAtom, remove: IJsonAtom }
141
315
  ```
142
316
 
143
- ##### Type Change Handling
317
+ #### `operationSpecDict` / `atomSpecDict` -- Spec Introspection
144
318
 
145
- ```javascript
146
- import { diff } from 'json-diff-ts';
319
+ Separate spec-defined fields from extension properties:
320
+
321
+ ```typescript
322
+ import { operationSpecDict, operationExtensions, atomSpecDict } from 'json-diff-ts';
323
+
324
+ const op = { op: 'replace', path: '$.name', value: 'Bob', x_author: 'system' };
325
+ operationSpecDict(op); // { op: 'replace', path: '$.name', value: 'Bob' }
326
+ operationExtensions(op); // { x_author: 'system' }
147
327
 
148
- // Control how type changes are treated
149
- const diffs = diff(oldData, newData, { treatTypeChangeAsReplace: false });
328
+ // Strip all extensions from an atom
329
+ const clean = atomSpecDict(atom);
150
330
  ```
151
331
 
152
- Date objects can now be updated to primitive values without errors when `treatTypeChangeAsReplace` is set to `false`.
332
+ #### `leafProperty` -- Path Introspection
153
333
 
154
- ##### Skip Nested Paths
334
+ Extract the terminal property name from an operation's path:
155
335
 
156
- ```javascript
157
- import { diff } from 'json-diff-ts';
336
+ ```typescript
337
+ import { leafProperty } from 'json-diff-ts';
158
338
 
159
- // Skip specific nested paths from comparison - useful for ignoring metadata
160
- const diffs = diff(oldData, newData, { keysToSkip: ['characters.metadata'] });
339
+ leafProperty({ op: 'replace', path: '$.user.name' }); // 'name'
340
+ leafProperty({ op: 'add', path: '$.items[?(@.id==1)]' }); // null (filter)
341
+ leafProperty({ op: 'replace', path: '$' }); // null (root)
161
342
  ```
162
343
 
163
- ##### Dynamic Key Resolution
344
+ ---
164
345
 
165
- ```javascript
166
- import { diff } from 'json-diff-ts';
346
+ ## Comparison Serialization
167
347
 
168
- // Use function to resolve object keys dynamically
169
- const diffs = diff(oldData, newData, {
170
- embeddedObjKeys: {
171
- characters: (obj, shouldReturnKeyName) => (shouldReturnKeyName ? 'id' : obj.id)
348
+ Serialize enriched comparison trees to plain objects or flat change lists.
349
+
350
+ ```typescript
351
+ import { compare, comparisonToDict, comparisonToFlatList } from 'json-diff-ts';
352
+
353
+ const result = compare(
354
+ { name: 'Alice', age: 30, role: 'viewer' },
355
+ { name: 'Bob', age: 30, status: 'active' }
356
+ );
357
+
358
+ // Recursive plain object
359
+ const dict = comparisonToDict(result);
360
+ // {
361
+ // type: 'CONTAINER',
362
+ // value: {
363
+ // name: { type: 'UPDATE', value: 'Bob', oldValue: 'Alice' },
364
+ // age: { type: 'UNCHANGED', value: 30 },
365
+ // role: { type: 'REMOVE', oldValue: 'viewer' },
366
+ // status: { type: 'ADD', value: 'active' }
367
+ // }
368
+ // }
369
+
370
+ // Flat list of leaf changes with paths
371
+ const flat = comparisonToFlatList(result);
372
+ // [
373
+ // { path: '$.name', type: 'UPDATE', value: 'Bob', oldValue: 'Alice' },
374
+ // { path: '$.role', type: 'REMOVE', oldValue: 'viewer' },
375
+ // { path: '$.status', type: 'ADD', value: 'active' }
376
+ // ]
377
+
378
+ // Include unchanged entries
379
+ const all = comparisonToFlatList(result, { includeUnchanged: true });
380
+ ```
381
+
382
+ ---
383
+
384
+ ## Practical Examples
385
+
386
+ ### Audit Log
387
+
388
+ Store every change to a document as a reversible atom. Each entry records who changed what, when, and can be replayed or reverted independently -- a complete JSON change tracking system:
389
+
390
+ ```typescript
391
+ import { diffAtom, applyAtom, revertAtom, IJsonAtom } from 'json-diff-ts';
392
+
393
+ interface AuditEntry {
394
+ timestamp: string;
395
+ userId: string;
396
+ atom: IJsonAtom;
397
+ }
398
+
399
+ const auditLog: AuditEntry[] = [];
400
+ let doc = {
401
+ title: 'Project Plan',
402
+ status: 'draft',
403
+ items: [
404
+ { id: 1, task: 'Design', done: false },
405
+ { id: 2, task: 'Build', done: false },
406
+ ],
407
+ };
408
+
409
+ function updateDocument(newDoc: typeof doc, userId: string) {
410
+ const atom = diffAtom(doc, newDoc, {
411
+ arrayIdentityKeys: { items: 'id' },
412
+ });
413
+
414
+ if (atom.operations.length > 0) {
415
+ auditLog.push({ timestamp: new Date().toISOString(), userId, atom });
416
+ doc = applyAtom(structuredClone(doc), atom);
172
417
  }
173
- });
418
+
419
+ return doc;
420
+ }
421
+
422
+ // Revert the last change
423
+ function undo(): typeof doc {
424
+ const last = auditLog.pop();
425
+ if (!last) return doc;
426
+ doc = revertAtom(structuredClone(doc), last.atom);
427
+ return doc;
428
+ }
429
+
430
+ // Example usage:
431
+ updateDocument(
432
+ { ...doc, status: 'active', items: [{ id: 1, task: 'Design', done: true }, ...doc.items.slice(1)] },
433
+ 'alice'
434
+ );
435
+ // auditLog[0].atom.operations =>
436
+ // [
437
+ // { op: 'replace', path: '$.status', value: 'active', oldValue: 'draft' },
438
+ // { op: 'replace', path: '$.items[?(@.id==1)].done', value: true, oldValue: false }
439
+ // ]
174
440
  ```
175
441
 
176
- ##### Regular Expression Paths
442
+ Because every atom is self-describing JSON, your audit log is queryable, storable in any database, and readable from any language.
177
443
 
178
- ```javascript
179
- import { diff } from 'json-diff-ts';
444
+ ### Undo / Redo Stack
445
+
446
+ Build undo/redo for any JSON state object. Atoms are small (only changed fields), reversible, and serializable:
447
+
448
+ ```typescript
449
+ import { diffAtom, applyAtom, revertAtom, IJsonAtom } from 'json-diff-ts';
450
+
451
+ class UndoManager<T extends object> {
452
+ private undoStack: IJsonAtom[] = [];
453
+ private redoStack: IJsonAtom[] = [];
454
+
455
+ constructor(private state: T) {}
456
+
457
+ apply(newState: T): T {
458
+ const atom = diffAtom(this.state, newState);
459
+ if (atom.operations.length === 0) return this.state;
460
+ this.undoStack.push(atom);
461
+ this.redoStack = [];
462
+ this.state = applyAtom(structuredClone(this.state), atom);
463
+ return this.state;
464
+ }
465
+
466
+ undo(): T {
467
+ const atom = this.undoStack.pop();
468
+ if (!atom) return this.state;
469
+ this.redoStack.push(atom);
470
+ this.state = revertAtom(structuredClone(this.state), atom);
471
+ return this.state;
472
+ }
473
+
474
+ redo(): T {
475
+ const atom = this.redoStack.pop();
476
+ if (!atom) return this.state;
477
+ this.undoStack.push(atom);
478
+ this.state = applyAtom(structuredClone(this.state), atom);
479
+ return this.state;
480
+ }
481
+ }
482
+ ```
483
+
484
+ ### Data Synchronization
485
+
486
+ Send only what changed between client and server. Atoms are compact -- a single field change in a 10KB document produces a few bytes of atom, making state synchronization efficient over the wire:
487
+
488
+ ```typescript
489
+ import { diffAtom, applyAtom, validateAtom } from 'json-diff-ts';
490
+
491
+ // Client side: compute and send atom
492
+ const atom = diffAtom(localState, updatedState, {
493
+ arrayIdentityKeys: { records: 'id' },
494
+ });
495
+ await fetch('/api/sync', {
496
+ method: 'POST',
497
+ body: JSON.stringify(atom),
498
+ });
180
499
 
181
- // Use regex for path matching - powerful for dynamic property names
182
- const embeddedObjKeys = new Map();
183
- embeddedObjKeys.set(/^characters/, 'id'); // Match any property starting with 'characters'
184
- const diffs = diff(oldData, newData, { embeddedObjKeys });
500
+ // Server side: validate and apply
501
+ const result = validateAtom(req.body);
502
+ if (!result.valid) return res.status(400).json(result.errors);
503
+ // ⚠️ In production, sanitize paths/values to prevent prototype pollution
504
+ // (e.g. reject paths containing "__proto__" or "constructor")
505
+ currentState = applyAtom(structuredClone(currentState), req.body);
185
506
  ```
186
507
 
187
- ##### String Array Comparison
508
+ ---
188
509
 
189
- ```javascript
190
- import { diff } from 'json-diff-ts';
510
+ ## Bridge: v4 Changeset <-> JSON Atom
511
+
512
+ Convert between the legacy internal format and JSON Atom:
513
+
514
+ ```typescript
515
+ import { diff, toAtom, fromAtom, unatomizeChangeset } from 'json-diff-ts';
516
+
517
+ // v4 changeset → JSON Atom
518
+ const changeset = diff(source, target, { arrayIdentityKeys: { items: 'id' } });
519
+ const atom = toAtom(changeset);
520
+
521
+ // JSON Atom → v4 atomic changes
522
+ const atoms = fromAtom(atom);
191
523
 
192
- // Compare string arrays by value instead of index - useful for tags, categories
193
- const diffs = diff(oldData, newData, { embeddedObjKeys: { equipment: '$value' } });
524
+ // v4 atomic changes hierarchical changeset (if needed)
525
+ const cs = unatomizeChangeset(atoms);
194
526
  ```
195
527
 
196
- ##### Array Move Detection
528
+ **Note:** `toAtom` is a best-effort bridge. Filter literals are always string-quoted (e.g., `[?(@.id=='42')]` instead of canonical `[?(@.id==42)]`). Use `diffAtom()` for fully canonical output.
197
529
 
198
- When working with arrays that have embedded object keys, you can enable detection of element reordering as MOVE operations:
530
+ ---
199
531
 
200
- ```javascript
532
+ ## Legacy Changeset API (v4 Compatibility)
533
+
534
+ All v4 APIs remain fully supported. Existing code continues to work without changes. For new projects, prefer the JSON Atom API above.
535
+
536
+ ### `diff`
537
+
538
+ Generates a hierarchical changeset between two objects:
539
+
540
+ ```typescript
201
541
  import { diff } from 'json-diff-ts';
202
542
 
203
- const before = {
204
- users: [
205
- { id: 1, name: 'Alice' },
206
- { id: 2, name: 'Bob' },
207
- { id: 3, name: 'Charlie' }
208
- ]
543
+ const oldData = {
544
+ location: 'Tatooine',
545
+ characters: [
546
+ { id: 'LUKE', name: 'Luke Skywalker', role: 'Farm Boy' },
547
+ { id: 'LEIA', name: 'Princess Leia', role: 'Prisoner' }
548
+ ],
209
549
  };
210
550
 
211
- const after = {
212
- users: [
213
- { id: 2, name: 'Bob' },
214
- { id: 3, name: 'Charlie' },
215
- { id: 1, name: 'Alice' }
216
- ]
551
+ const newData = {
552
+ location: 'Yavin Base',
553
+ characters: [
554
+ { id: 'LUKE', name: 'Luke Skywalker', role: 'Pilot', rank: 'Commander' },
555
+ { id: 'HAN', name: 'Han Solo', role: 'Smuggler' }
556
+ ],
217
557
  };
218
558
 
219
- // Without move detection (default behavior)
220
- const diffsNoMoves = diff(before, after, { embeddedObjKeys: { users: 'id' } });
221
- console.log(diffsNoMoves); // [] - no changes detected since elements are unchanged
222
-
223
- // With move detection enabled
224
- const diffsWithMoves = diff(before, after, {
225
- embeddedObjKeys: { users: 'id' },
226
- detectArrayMoves: true
227
- });
228
- console.log(diffsWithMoves);
229
- // [
230
- // {
231
- // type: 'UPDATE',
232
- // key: 'users',
233
- // embeddedKey: 'id',
234
- // changes: [
235
- // { type: 'MOVE', key: 2, oldIndex: 1, newIndex: 0, value: { id: 2, name: 'Bob' } },
236
- // { type: 'MOVE', key: 3, oldIndex: 2, newIndex: 1, value: { id: 3, name: 'Charlie' } },
237
- // { type: 'MOVE', key: 1, oldIndex: 0, newIndex: 2, value: { id: 1, name: 'Alice' } }
238
- // ]
239
- // }
240
- // ]
559
+ const changes = diff(oldData, newData, { arrayIdentityKeys: { characters: 'id' } });
241
560
  ```
242
561
 
243
- This feature is particularly useful for:
244
- - **UI reordering**: Drag-and-drop interfaces, sortable lists
245
- - **Priority changes**: Task lists, menu items
246
- - **Data synchronization**: Tracking positional changes in ordered collections
562
+ ### `applyChangeset` and `revertChangeset`
563
+
564
+ ```typescript
565
+ import { applyChangeset, revertChangeset } from 'json-diff-ts';
566
+
567
+ const updated = applyChangeset(structuredClone(oldData), changes);
568
+ const reverted = revertChangeset(structuredClone(newData), changes);
569
+ ```
247
570
 
248
571
  ### `atomizeChangeset` and `unatomizeChangeset`
249
572
 
250
- Transform complex changesets into a list of atomic changes (and back), each describable by a JSONPath.
573
+ Flatten a hierarchical changeset into atomic changes addressable by JSONPath, or reconstruct the hierarchy:
251
574
 
252
- ```javascript
575
+ ```typescript
253
576
  import { atomizeChangeset, unatomizeChangeset } from 'json-diff-ts';
254
577
 
255
- // Create atomic changes
256
- const atomicChanges = atomizeChangeset(diffs);
257
-
258
- // Restore the changeset from a selection of atomic changes
259
- const changeset = unatomizeChangeset(atomicChanges.slice(0, 3));
260
- ```
261
-
262
- **Atomic Changes Structure:**
263
-
264
- ```javascript
265
- [
266
- {
267
- type: 'UPDATE',
268
- key: 'location',
269
- value: 'Yavin Base',
270
- oldValue: 'Tatooine',
271
- path: '$.location',
272
- valueType: 'String'
273
- },
274
- {
275
- type: 'UPDATE',
276
- key: 'mission',
277
- value: 'Destroy Death Star',
278
- oldValue: 'Rescue Princess',
279
- path: '$.mission',
280
- valueType: 'String'
281
- },
282
- {
283
- type: 'ADD',
284
- key: 'rank',
285
- value: 'Commander',
286
- path: "$.characters[?(@.id=='LUKE_SKYWALKER')].rank",
287
- valueType: 'String'
288
- },
289
- {
290
- type: 'ADD',
291
- key: 'HAN_SOLO',
292
- value: { id: 'HAN_SOLO', name: 'Han Solo', role: 'Smuggler', forceTraining: false, ship: 'Millennium Falcon' },
293
- path: "$.characters[?(@.id=='HAN_SOLO')]",
294
- valueType: 'Object'
295
- }
296
- ]
578
+ const atoms = atomizeChangeset(changes);
579
+ // [
580
+ // { type: 'UPDATE', key: 'location', value: 'Yavin Base', oldValue: 'Tatooine',
581
+ // path: '$.location', valueType: 'String' },
582
+ // { type: 'ADD', key: 'rank', value: 'Commander',
583
+ // path: "$.characters[?(@.id=='LUKE')].rank", valueType: 'String' },
584
+ // ...
585
+ // ]
586
+
587
+ const restored = unatomizeChangeset(atoms.slice(0, 2));
297
588
  ```
298
589
 
299
- ### `applyChangeset` and `revertChangeset`
590
+ ### Advanced Options
300
591
 
301
- Apply or revert changes to JSON objects.
592
+ #### Key-based Array Matching
302
593
 
303
- ```javascript
304
- import { applyChangeset, revertChangeset } from 'json-diff-ts';
594
+ ```typescript
595
+ // Named key
596
+ diff(old, new, { arrayIdentityKeys: { characters: 'id' } });
305
597
 
306
- // Apply changes
307
- const updated = applyChangeset(oldData, diffs);
308
- console.log(updated);
309
- // { location: 'Yavin Base', mission: 'Destroy Death Star', status: 'Complete', ... }
598
+ // Function key
599
+ diff(old, new, {
600
+ arrayIdentityKeys: {
601
+ characters: (obj, shouldReturnKeyName) => (shouldReturnKeyName ? 'id' : obj.id)
602
+ }
603
+ });
310
604
 
311
- // Revert changes
312
- const reverted = revertChangeset(newData, diffs);
313
- console.log(reverted);
314
- // { location: 'Tatooine', mission: 'Rescue Princess', status: 'In Progress', ... }
605
+ // Regex path matching
606
+ const keys = new Map();
607
+ keys.set(/^characters/, 'id');
608
+ diff(old, new, { arrayIdentityKeys: keys });
609
+
610
+ // Value-based identity for primitive arrays
611
+ diff(old, new, { arrayIdentityKeys: { tags: '$value' } });
315
612
  ```
316
613
 
317
- ## API Reference
614
+ #### Path Skipping
318
615
 
319
- ### Core Functions
616
+ ```typescript
617
+ diff(old, new, { keysToSkip: ['characters.metadata'] });
618
+ ```
320
619
 
321
- | Function | Description | Parameters |
322
- |----------|-------------|------------|
323
- | `diff(oldObj, newObj, options?)` | Generate differences between two objects | `oldObj`: Original object<br>`newObj`: Updated object<br>`options`: Optional configuration |
324
- | `applyChangeset(obj, changeset)` | Apply changes to an object | `obj`: Object to modify<br>`changeset`: Changes to apply |
325
- | `revertChangeset(obj, changeset)` | Revert changes from an object | `obj`: Object to modify<br>`changeset`: Changes to revert |
326
- | `atomizeChangeset(changeset)` | Convert changeset to atomic changes | `changeset`: Nested changeset |
327
- | `unatomizeChangeset(atomicChanges)` | Convert atomic changes back to nested changeset | `atomicChanges`: Array of atomic changes |
620
+ #### Type Change Handling
621
+
622
+ ```typescript
623
+ diff(old, new, { treatTypeChangeAsReplace: false });
624
+ ```
625
+
626
+ ### Legacy API Reference
627
+
628
+ | Function | Description |
629
+ | --- | --- |
630
+ | `diff(oldObj, newObj, options?)` | Compute hierarchical changeset |
631
+ | `applyChangeset(obj, changeset)` | Apply a changeset to an object |
632
+ | `revertChangeset(obj, changeset)` | Revert a changeset from an object |
633
+ | `atomizeChangeset(changeset)` | Flatten to atomic changes with JSONPath |
634
+ | `unatomizeChangeset(atoms)` | Reconstruct hierarchy from atomic changes |
328
635
 
329
636
  ### Comparison Functions
330
637
 
331
- | Function | Description | Parameters |
332
- |----------|-------------|------------|
333
- | `compare(oldObj, newObj)` | Create enriched comparison object | `oldObj`: Original object<br>`newObj`: Updated object |
334
- | `enrich(obj)` | Create enriched representation of object | `obj`: Object to enrich |
335
- | `createValue(value)` | Create value node for comparison | `value`: Any value |
336
- | `createContainer(value)` | Create container node for comparison | `value`: Object or Array |
638
+ | Function | Description |
639
+ | --- | --- |
640
+ | `compare(oldObj, newObj)` | Create enriched comparison object |
641
+ | `enrich(obj)` | Create enriched representation |
642
+ | `comparisonToDict(node)` | Serialize comparison tree to plain object |
643
+ | `comparisonToFlatList(node, options?)` | Flatten comparison to leaf change list |
337
644
 
338
- ### Options Interface
645
+ ### Options
339
646
 
340
647
  ```typescript
341
648
  interface Options {
342
- embeddedObjKeys?: Record<string, string | Function> | Map<string | RegExp, string | Function>;
343
- keysToSkip?: string[];
344
- treatTypeChangeAsReplace?: boolean;
345
- detectArrayMoves?: boolean;
649
+ arrayIdentityKeys?: Record<string, string | FunctionKey> | Map<string | RegExp, string | FunctionKey>;
650
+ /** @deprecated Use arrayIdentityKeys instead */
651
+ embeddedObjKeys?: Record<string, string | FunctionKey> | Map<string | RegExp, string | FunctionKey>;
652
+ keysToSkip?: readonly string[];
653
+ treatTypeChangeAsReplace?: boolean; // default: true
346
654
  }
347
655
  ```
348
656
 
349
- | Option | Type | Description |
350
- | ------ | ---- | ----------- |
351
- | `embeddedObjKeys` | `Record<string, string | Function>` or `Map<string | RegExp, string | Function>` | Map paths of arrays to a key or resolver function used to match elements when diffing. Use a `Map` for regex paths. |
352
- | `keysToSkip` | `string[]` | Dotted paths to exclude from comparison, e.g. `"meta.info"`. |
353
- | `treatTypeChangeAsReplace` | `boolean` | When `true` (default), a type change results in a REMOVE/ADD pair. Set to `false` to treat it as an UPDATE. |
354
- | `detectArrayMoves` | `boolean` | When `true`, detect array element reordering as MOVE operations. Only works with `embeddedObjKeys`. Default: `false`. |
657
+ ---
355
658
 
356
- ### Change Types
659
+ ## Migration from v4
357
660
 
358
- ```typescript
359
- enum Operation {
360
- REMOVE = 'REMOVE',
361
- ADD = 'ADD',
362
- UPDATE = 'UPDATE',
363
- MOVE = 'MOVE'
364
- }
365
- ```
661
+ 1. **No action required** -- all v4 APIs work identically in v5.
662
+ 2. **Adopt JSON Atom** -- use `diffAtom()` / `applyAtom()` for new code.
663
+ 3. **Bridge existing data** -- `toAtom()` / `fromAtom()` for interop with stored v4 changesets.
664
+ 4. **Rename `embeddedObjKeys` to `arrayIdentityKeys`** -- the old name still works, but `arrayIdentityKeys` is the preferred name going forward.
665
+ 5. Both formats coexist. No forced migration.
666
+
667
+ ---
668
+
669
+ ## Why json-diff-ts?
670
+
671
+ | Feature | json-diff-ts | deep-diff | jsondiffpatch | RFC 6902 |
672
+ | --- | --- | --- | --- | --- |
673
+ | TypeScript | Native | Partial | Definitions only | Varies |
674
+ | Bundle Size | ~21KB | ~45KB | ~120KB+ | Varies |
675
+ | Dependencies | Zero | Few | Many | Varies |
676
+ | ESM Support | Native | CJS only | CJS only | Varies |
677
+ | Array Identity | Key-based | Index only | Configurable | Index only |
678
+ | Wire Format | JSON Atom (standardized) | Proprietary | Proprietary | JSON Pointer |
679
+ | Reversibility | Built-in (`oldValue`) | Manual | Plugin | Not built-in |
680
+
681
+ ## FAQ
682
+
683
+ **Q: How does JSON Atom compare to JSON Patch (RFC 6902)?**
684
+ JSON Patch uses JSON Pointer (`/items/0`) for paths, which breaks when array elements are inserted, deleted, or reordered. JSON Atom uses JSONPath filter expressions (`$.items[?(@.id==1)]`) for stable, key-based identity. JSON Atom also supports built-in reversibility via `oldValue`.
685
+
686
+ **Q: Can I use this with React / Vue / Angular?**
687
+ Yes. json-diff-ts works in any JavaScript runtime -- browsers, Node.js, Deno, Bun, edge workers.
688
+
689
+ **Q: Is it suitable for large objects?**
690
+ Yes. The library handles large, deeply nested JSON structures efficiently with zero dependencies and a ~6KB gzipped footprint.
691
+
692
+ **Q: Can I use the v4 API alongside JSON Atom?**
693
+ Yes. Both APIs coexist. Use `toAtom()` / `fromAtom()` to convert between formats.
694
+
695
+ **Q: What about arrays of primitives?**
696
+ Use `$value` as the identity key: `{ arrayIdentityKeys: { tags: '$value' } }`. Elements are matched by value identity.
697
+
698
+ ---
366
699
 
367
700
  ## Release Notes
368
701
 
369
- - **v4.9.0:** Enhanced array handling for `undefined` values - arrays with `undefined` elements can now be properly reconstructed from changesets. Fixed issue where transitions to `undefined` in arrays were treated as removals instead of updates (fixes issue #316)
702
+ - **v5.0.0-alpha.2:**
703
+ - Atom workflow helpers: `squashAtoms`, `atomMap`, `atomStamp`, `atomGroupBy`
704
+ - Atom/operation introspection: `operationSpecDict`, `operationExtensions`, `atomSpecDict`, `atomExtensions`, `leafProperty`
705
+ - Comparison serialization: `comparisonToDict`, `comparisonToFlatList`
706
+
707
+ - **v5.0.0-alpha.0:**
708
+ - JSON Atom API: `diffAtom`, `applyAtom`, `revertAtom`, `invertAtom`, `toAtom`, `fromAtom`, `validateAtom`
709
+ - Canonical path production with typed filter literals
710
+ - Conformance with the [JSON Atom Specification](https://github.com/ltwlf/json-atom-format) v0
711
+ - Renamed `embeddedObjKeys` to `arrayIdentityKeys` (old name still works as deprecated alias)
712
+ - All v4 APIs preserved unchanged
713
+
714
+ - **v4.9.0:**
715
+ - Fixed `applyChangeset` and `revertChangeset` for root-level arrays containing objects (fixes #362)
716
+ - Fixed `compare` on root-level arrays producing unexpected UNCHANGED entries (fixes #358)
717
+ - Refactored `applyChangelist` path resolution for correctness with terminal array indices
718
+ - `keysToSkip` now accepts `readonly string[]` (fixes #359)
719
+ - `keyBy` callback now receives the element index (PR #365)
720
+ - Enhanced array handling for `undefined` values (fixes #316)
721
+ - Fixed typo in warning message (#361)
722
+ - Fixed README Options Interface formatting (#360)
370
723
  - **v4.8.2:** Fixed array handling in `applyChangeset` for null, undefined, and deleted elements (fixes issue #316)
371
724
  - **v4.8.1:** Improved documentation with working examples and detailed options.
372
- - **v4.8.0:** Significantly reduced bundle size by completely removing es-toolkit dependency and implementing custom utility functions. This change eliminates external dependencies while maintaining identical functionality and improving performance.
373
-
725
+ - **v4.8.0:** Significantly reduced bundle size by completely removing es-toolkit dependency and implementing custom utility functions.
374
726
  - **v4.7.0:** Optimized bundle size and performance by replacing es-toolkit/compat with es-toolkit for difference, intersection, and keyBy functions
375
-
376
727
  - **v4.6.3:** Fixed null comparison returning update when values are both null (fixes issue #284)
377
-
378
- - **v4.6.2:** Fixed updating to null when `treatTypeChangeAsReplace` is false and bumped Jest dev dependencies
728
+ - **v4.6.2:** Fixed updating to null when `treatTypeChangeAsReplace` is false
379
729
  - **v4.6.1:** Consistent JSONPath format for array items (fixes issue #269)
380
730
  - **v4.6.0:** Fixed filter path regex to avoid polynomial complexity
381
- - **v4.5.1:** Updated package dependencies
382
731
  - **v4.5.0:** Switched internal utilities from lodash to es-toolkit/compat for a smaller bundle size
383
732
  - **v4.4.0:** Fixed Date-to-string diff when `treatTypeChangeAsReplace` is false
384
- - **v4.3.0:** Enhanced functionality:
385
- - Added support for nested keys to skip using dotted path notation in the keysToSkip option
386
- - This allows excluding specific nested object paths from comparison (fixes #242)
387
- - **v4.2.0:** Improved stability with multiple fixes:
388
- - Fixed object handling in atomizeChangeset and unatomizeChangeset
389
- - Fixed array handling in applyChangeset and revertChangeset
390
- - Fixed handling of null values in applyChangeset
391
- - Fixed handling of empty REMOVE operations when diffing from undefined
733
+ - **v4.3.0:** Added support for nested keys to skip using dotted path notation (fixes #242)
734
+ - **v4.2.0:** Improved stability with multiple fixes for atomize/unatomize, apply/revert, null handling
392
735
  - **v4.1.0:** Full support for ES modules while maintaining CommonJS compatibility
393
- - **v4.0.0:** Changed naming of flattenChangeset and unflattenChanges to atomizeChangeset and unatomizeChangeset; added option to set treatTypeChangeAsReplace
394
- - **v3.0.1:** Fixed issue with unflattenChanges when a key has periods
395
- - **v3.0.0:** Added support for both CommonJS and ECMAScript Modules. Replaced lodash-es with lodash to support both module formats
396
- - **v5.0.0-alpha.1:** Added MOVE operation for array reordering detection with embeddedObjKeys. New `detectArrayMoves` option enables tracking of element position changes within arrays when using key-based identification. Fully backward compatible.
397
- - **v4.8.2:** Latest stable release
398
- - **v2.2.0:** Fixed lodash-es dependency, added exclude keys option, added string array comparison by value
399
- - **v2.1.0:** Fixed JSON Path filters by replacing single equal sign (=) with double equal sign (==). Added support for using '.' as root in paths
400
- - **v2.0.0:** Upgraded to ECMAScript module format with optimizations and improved documentation. Fixed regex path handling (breaking change: now requires Map instead of Record for regex paths)
401
- - **v1.2.6:** Enhanced JSON Path handling for period-inclusive segments
402
- - **v1.2.5:** Added key name resolution support for key functions
403
- - **v1.2.4:** Documentation updates and dependency upgrades
404
- - **v1.2.3:** Updated dependencies and TypeScript
736
+ - **v4.0.0:** Renamed flattenChangeset/unflattenChanges to atomizeChangeset/unatomizeChangeset; added treatTypeChangeAsReplace option
405
737
 
406
738
  ## Contributing
407
739
 
408
740
  Contributions are welcome! Please follow the provided issue templates and code of conduct.
409
741
 
410
- ## Performance & Bundle Size
411
-
412
- - **Zero dependencies**: No external runtime dependencies
413
- - **Lightweight**: ~21KB minified, ~6KB gzipped
414
- - **Tree-shakable**: Use only what you need with ES modules
415
- - **High performance**: Optimized for large JSON objects and arrays
416
-
417
- ## Use Cases
418
-
419
- - **State Management**: Track changes in Redux, Zustand, or custom state stores
420
- - **Form Handling**: Detect field changes in React, Vue, or Angular forms
421
- - **Data Synchronization**: Sync data between client and server efficiently
422
- - **Version Control**: Implement undo/redo functionality
423
- - **API Optimization**: Send only changed data to reduce bandwidth
424
- - **Real-time Updates**: Track changes in collaborative applications
742
+ ## Support
425
743
 
426
- ## Comparison with Alternatives
744
+ If you find this library useful, consider supporting its development:
427
745
 
428
- | Feature | json-diff-ts | deep-diff | jsondiffpatch |
429
- |---------|--------------|-----------|---------------|
430
- | TypeScript | ✅ Native | ❌ Partial | ❌ Definitions only |
431
- | Bundle Size | 🟢 21KB | 🟡 45KB | 🔴 120KB+ |
432
- | Dependencies | 🟢 Zero | 🟡 Few | 🔴 Many |
433
- | ESM Support | ✅ Native | ❌ CJS only | ❌ CJS only |
434
- | Array Key Matching | ✅ Advanced | ❌ Basic | ✅ Advanced |
435
- | JSONPath Support | ✅ Full | ❌ None | ❌ Limited |
436
-
437
- ## FAQ
438
-
439
- **Q: Can I use this with React/Vue/Angular?**
440
- A: Yes! json-diff-ts works with any JavaScript framework or vanilla JS.
441
-
442
- **Q: Does it work with Node.js?**
443
- A: Absolutely! Supports Node.js 18+ with both CommonJS and ES modules.
444
-
445
- **Q: How does it compare to JSON Patch (RFC 6902)?**
446
- A: json-diff-ts provides a more flexible format with advanced array handling, while JSON Patch is a standardized format.
447
-
448
- **Q: Is it suitable for large objects?**
449
- A: Yes, the library is optimized for performance and can handle large, complex JSON structures efficiently.
746
+ [![Buy Me A Coffee](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://buymeacoffee.com/leitwolf)
450
747
 
451
748
  ## Contact
452
749
 
453
- Reach out to the maintainer:
454
-
455
750
  - LinkedIn: [Christian Glessner](https://www.linkedin.com/in/christian-glessner/)
456
751
  - Twitter: [@leitwolf_io](https://twitter.com/leitwolf_io)
457
752