json-diff-ts 5.0.0-alpha.2 → 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 +137 -137
- package/dist/index.cjs +188 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -58
- package/dist/index.d.ts +58 -58
- package/dist/index.js +173 -137
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
13
|
[](https://buymeacoffee.com/leitwolf)
|
|
14
14
|
|
|
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
|
|
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.
|
|
16
16
|
|
|
17
17
|
Zero dependencies. TypeScript-first. ESM + CommonJS. Trusted by thousands of developers ([500K+ weekly npm downloads](https://www.npmjs.com/package/json-diff-ts)).
|
|
18
18
|
|
|
@@ -32,7 +32,7 @@ This makes diffs fragile -- you can't store them, replay them reliably, or build
|
|
|
32
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
33
|
|
|
34
34
|
```typescript
|
|
35
|
-
import {
|
|
35
|
+
import { diffAtom, applyAtom, revertAtom } from 'json-diff-ts';
|
|
36
36
|
|
|
37
37
|
const before = {
|
|
38
38
|
items: [
|
|
@@ -49,14 +49,14 @@ const after = {
|
|
|
49
49
|
],
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
const
|
|
52
|
+
const atom = diffAtom(before, after, { arrayIdentityKeys: { items: 'id' } });
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The
|
|
55
|
+
The atom tracks _what_ changed, not _where_ it moved:
|
|
56
56
|
|
|
57
57
|
```json
|
|
58
58
|
{
|
|
59
|
-
"format": "json-
|
|
59
|
+
"format": "json-atom",
|
|
60
60
|
"version": 1,
|
|
61
61
|
"operations": [
|
|
62
62
|
{ "op": "replace", "path": "$.items[?(@.id==1)].name", "value": "Widget Pro", "oldValue": "Widget" },
|
|
@@ -69,15 +69,15 @@ The delta tracks _what_ changed, not _where_ it moved:
|
|
|
69
69
|
Apply forward to get the new state, or revert to restore the original:
|
|
70
70
|
|
|
71
71
|
```typescript
|
|
72
|
-
// Clone before applying —
|
|
73
|
-
const updated =
|
|
74
|
-
const restored =
|
|
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
75
|
```
|
|
76
76
|
|
|
77
77
|
## Quick Start
|
|
78
78
|
|
|
79
79
|
```typescript
|
|
80
|
-
import {
|
|
80
|
+
import { diffAtom, applyAtom, revertAtom } from 'json-diff-ts';
|
|
81
81
|
|
|
82
82
|
const oldObj = {
|
|
83
83
|
items: [
|
|
@@ -94,24 +94,24 @@ const newObj = {
|
|
|
94
94
|
],
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
// 1. Compute
|
|
98
|
-
const
|
|
97
|
+
// 1. Compute an atom between two JSON objects
|
|
98
|
+
const atom = diffAtom(oldObj, newObj, {
|
|
99
99
|
arrayIdentityKeys: { items: 'id' }, // match array elements by 'id' field
|
|
100
100
|
});
|
|
101
|
-
//
|
|
101
|
+
// atom.operations =>
|
|
102
102
|
// [
|
|
103
103
|
// { op: 'replace', path: '$.items[?(@.id==1)].name', value: 'Widget Pro', oldValue: 'Widget' },
|
|
104
104
|
// { op: 'add', path: '$.items[?(@.id==3)]', value: { id: 3, name: 'Doohickey', price: 4.99 } }
|
|
105
105
|
// ]
|
|
106
106
|
|
|
107
|
-
// 2. Apply the
|
|
108
|
-
const updated =
|
|
107
|
+
// 2. Apply the atom to produce the new state
|
|
108
|
+
const updated = applyAtom(structuredClone(oldObj), atom);
|
|
109
109
|
|
|
110
|
-
// 3. Revert the
|
|
111
|
-
const reverted =
|
|
110
|
+
// 3. Revert the atom to restore the original state
|
|
111
|
+
const reverted = revertAtom(structuredClone(updated), atom);
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
That's it. `
|
|
114
|
+
That's it. `atom` is a plain JSON object you can store in a database, send over HTTP, or consume in any language.
|
|
115
115
|
|
|
116
116
|
## Installation
|
|
117
117
|
|
|
@@ -121,58 +121,58 @@ npm install json-diff-ts
|
|
|
121
121
|
|
|
122
122
|
```typescript
|
|
123
123
|
// ESM / TypeScript
|
|
124
|
-
import {
|
|
124
|
+
import { diffAtom, applyAtom, revertAtom } from 'json-diff-ts';
|
|
125
125
|
|
|
126
126
|
// CommonJS
|
|
127
|
-
const {
|
|
127
|
+
const { diffAtom, applyAtom, revertAtom } = require('json-diff-ts');
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
## What is JSON
|
|
130
|
+
## What is JSON Atom?
|
|
131
131
|
|
|
132
|
-
[JSON
|
|
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
133
|
|
|
134
134
|
```text
|
|
135
|
-
json-
|
|
135
|
+
json-atom-format (specification)
|
|
136
136
|
├── json-diff-ts (TypeScript implementation) ← this package
|
|
137
|
-
└── json-
|
|
137
|
+
└── json-atom-py (Python implementation)
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
The specification defines the wire format. Each language implementation produces and consumes compatible
|
|
140
|
+
The specification defines the wire format. Each language implementation produces and consumes compatible atoms.
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
An atom is a self-describing JSON document you can store, transmit, and consume in any language:
|
|
143
143
|
|
|
144
144
|
- **Three operations** -- `add`, `remove`, `replace`. Nothing else to learn.
|
|
145
145
|
- **JSONPath-based paths** -- `$.items[?(@.id==1)].name` identifies elements by key, not index.
|
|
146
146
|
- **Reversible by default** -- every `replace` and `remove` includes `oldValue` for undo.
|
|
147
|
-
- **Self-identifying** -- the `format` field makes
|
|
147
|
+
- **Self-identifying** -- the `format` field makes atoms discoverable without external context.
|
|
148
148
|
- **Extension-friendly** -- unknown properties are preserved; `x_`-prefixed properties are future-safe.
|
|
149
149
|
|
|
150
|
-
### JSON
|
|
150
|
+
### JSON Atom vs JSON Patch (RFC 6902)
|
|
151
151
|
|
|
152
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
153
|
|
|
154
|
-
JSON
|
|
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
155
|
|
|
156
|
-
| | JSON
|
|
156
|
+
| | JSON Atom | JSON Patch (RFC 6902) |
|
|
157
157
|
| --- | --- | --- |
|
|
158
158
|
| Path syntax | JSONPath (`$.items[?(@.id==1)]`) | JSON Pointer (`/items/0`) |
|
|
159
159
|
| Array identity | Key-based -- survives reorder | Index-based -- breaks on insert/delete |
|
|
160
160
|
| Reversibility | Built-in `oldValue` | Not supported |
|
|
161
161
|
| Self-describing | `format` field in envelope | No envelope |
|
|
162
|
-
| Specification | [json-
|
|
162
|
+
| Specification | [json-atom-format](https://github.com/ltwlf/json-atom-format) | [RFC 6902](https://tools.ietf.org/html/rfc6902) |
|
|
163
163
|
|
|
164
164
|
---
|
|
165
165
|
|
|
166
|
-
## JSON
|
|
166
|
+
## JSON Atom API
|
|
167
167
|
|
|
168
|
-
### `
|
|
168
|
+
### `diffAtom` -- Compute an Atom
|
|
169
169
|
|
|
170
170
|
```typescript
|
|
171
|
-
const
|
|
171
|
+
const atom = diffAtom(
|
|
172
172
|
{ user: { name: 'Alice', role: 'viewer' } },
|
|
173
173
|
{ user: { name: 'Alice', role: 'admin' } }
|
|
174
174
|
);
|
|
175
|
-
//
|
|
175
|
+
// atom.operations → [{ op: 'replace', path: '$.user.role', value: 'admin', oldValue: 'viewer' }]
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
#### Keyed Arrays
|
|
@@ -180,12 +180,12 @@ const delta = diffDelta(
|
|
|
180
180
|
Match array elements by identity key. Filter paths use canonical typed literals per the spec:
|
|
181
181
|
|
|
182
182
|
```typescript
|
|
183
|
-
const
|
|
183
|
+
const atom = diffAtom(
|
|
184
184
|
{ users: [{ id: 1, role: 'viewer' }, { id: 2, role: 'editor' }] },
|
|
185
185
|
{ users: [{ id: 1, role: 'admin' }, { id: 2, role: 'editor' }] },
|
|
186
186
|
{ arrayIdentityKeys: { users: 'id' } }
|
|
187
187
|
);
|
|
188
|
-
//
|
|
188
|
+
// atom.operations → [{ op: 'replace', path: '$.users[?(@.id==1)].role', value: 'admin', oldValue: 'viewer' }]
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
#### Non-reversible Mode
|
|
@@ -193,140 +193,140 @@ const delta = diffDelta(
|
|
|
193
193
|
Omit `oldValue` fields when you don't need undo:
|
|
194
194
|
|
|
195
195
|
```typescript
|
|
196
|
-
const
|
|
196
|
+
const atom = diffAtom(source, target, { reversible: false });
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
### `
|
|
199
|
+
### `applyAtom` -- Apply an Atom
|
|
200
200
|
|
|
201
201
|
Applies operations sequentially. Always use the return value (required for root-level replacements):
|
|
202
202
|
|
|
203
203
|
```typescript
|
|
204
|
-
const result =
|
|
204
|
+
const result = applyAtom(structuredClone(source), atom);
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
-
### `
|
|
207
|
+
### `revertAtom` -- Revert an Atom
|
|
208
208
|
|
|
209
209
|
Computes the inverse and applies it. Requires `oldValue` on all `replace` and `remove` operations:
|
|
210
210
|
|
|
211
211
|
```typescript
|
|
212
|
-
const original =
|
|
212
|
+
const original = revertAtom(structuredClone(target), atom);
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
### `
|
|
215
|
+
### `invertAtom` -- Compute the Inverse
|
|
216
216
|
|
|
217
|
-
Returns a new
|
|
217
|
+
Returns a new atom that undoes the original (spec Section 9.2):
|
|
218
218
|
|
|
219
219
|
```typescript
|
|
220
|
-
const inverse =
|
|
220
|
+
const inverse = invertAtom(atom);
|
|
221
221
|
// add ↔ remove, replace swaps value/oldValue, order reversed
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
### `
|
|
224
|
+
### `validateAtom` -- Validate Structure
|
|
225
225
|
|
|
226
226
|
```typescript
|
|
227
|
-
const { valid, errors } =
|
|
227
|
+
const { valid, errors } = validateAtom(maybeAtom);
|
|
228
228
|
```
|
|
229
229
|
|
|
230
230
|
### API Reference
|
|
231
231
|
|
|
232
232
|
| Function | Signature | Description |
|
|
233
233
|
| --- | --- | --- |
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
244
|
-
| `
|
|
245
|
-
| `operationSpecDict` | `(op) =>
|
|
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
246
|
| `operationExtensions` | `(op) => Record<string, any>` | Get extension properties from operation |
|
|
247
|
-
| `
|
|
248
|
-
| `
|
|
247
|
+
| `atomSpecDict` | `(atom) => IJsonAtom` | Strip all extensions from atom |
|
|
248
|
+
| `atomExtensions` | `(atom) => Record<string, any>` | Get envelope extensions from atom |
|
|
249
249
|
| `leafProperty` | `(op) => string \| null` | Terminal property name from operation path |
|
|
250
250
|
|
|
251
|
-
###
|
|
251
|
+
### AtomOptions
|
|
252
252
|
|
|
253
253
|
Extends the base `Options` interface:
|
|
254
254
|
|
|
255
255
|
```typescript
|
|
256
|
-
interface
|
|
256
|
+
interface AtomOptions extends Options {
|
|
257
257
|
reversible?: boolean; // Include oldValue for undo. Default: true
|
|
258
258
|
arrayIdentityKeys?: Record<string, string | FunctionKey>;
|
|
259
259
|
keysToSkip?: readonly string[];
|
|
260
260
|
}
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
-
###
|
|
263
|
+
### Atom Workflow Helpers
|
|
264
264
|
|
|
265
|
-
Transform, inspect, and compact
|
|
265
|
+
Transform, inspect, and compact atoms for workflow automation.
|
|
266
266
|
|
|
267
|
-
#### `
|
|
267
|
+
#### `squashAtoms` -- Compact Multiple Atoms
|
|
268
268
|
|
|
269
|
-
Combine a sequence of
|
|
269
|
+
Combine a sequence of atoms into a single net-effect atom. Useful for compacting audit logs or collapsing undo history:
|
|
270
270
|
|
|
271
271
|
```typescript
|
|
272
|
-
import {
|
|
272
|
+
import { diffAtom, applyAtom, squashAtoms } from 'json-diff-ts';
|
|
273
273
|
|
|
274
274
|
const source = { name: 'Alice', role: 'viewer' };
|
|
275
|
-
const d1 =
|
|
276
|
-
const d2 =
|
|
275
|
+
const d1 = diffAtom(source, { name: 'Bob', role: 'viewer' });
|
|
276
|
+
const d2 = diffAtom({ name: 'Bob', role: 'viewer' }, { name: 'Bob', role: 'admin' });
|
|
277
277
|
|
|
278
|
-
const squashed =
|
|
278
|
+
const squashed = squashAtoms(source, [d1, d2]);
|
|
279
279
|
// squashed.operations => [
|
|
280
280
|
// { op: 'replace', path: '$.name', value: 'Bob', oldValue: 'Alice' },
|
|
281
281
|
// { op: 'replace', path: '$.role', value: 'admin', oldValue: 'viewer' }
|
|
282
282
|
// ]
|
|
283
283
|
|
|
284
|
-
// Verify: applying the squashed
|
|
285
|
-
const result =
|
|
284
|
+
// Verify: applying the squashed atom equals applying both sequentially
|
|
285
|
+
const result = applyAtom(structuredClone(source), squashed);
|
|
286
286
|
// result => { name: 'Bob', role: 'admin' }
|
|
287
287
|
```
|
|
288
288
|
|
|
289
289
|
Options: `reversible`, `arrayIdentityKeys`, `target` (pre-computed final state), `verifyTarget` (default: true).
|
|
290
290
|
|
|
291
|
-
#### `
|
|
291
|
+
#### `atomMap` / `atomStamp` / `atomGroupBy` -- Atom Transformations
|
|
292
292
|
|
|
293
|
-
All transforms are immutable — they return new
|
|
293
|
+
All transforms are immutable — they return new atoms without modifying the original:
|
|
294
294
|
|
|
295
295
|
```typescript
|
|
296
|
-
import {
|
|
296
|
+
import { diffAtom, atomMap, atomStamp, atomGroupBy } from 'json-diff-ts';
|
|
297
297
|
|
|
298
|
-
const
|
|
298
|
+
const atom = diffAtom(
|
|
299
299
|
{ name: 'Alice', age: 30, role: 'viewer' },
|
|
300
300
|
{ name: 'Bob', age: 31, status: 'active' }
|
|
301
301
|
);
|
|
302
302
|
|
|
303
303
|
// Stamp metadata onto every operation
|
|
304
|
-
const stamped =
|
|
304
|
+
const stamped = atomStamp(atom, { x_author: 'system', x_ts: Date.now() });
|
|
305
305
|
|
|
306
306
|
// Transform operations
|
|
307
|
-
const prefixed =
|
|
307
|
+
const prefixed = atomMap(atom, (op) => ({
|
|
308
308
|
...op,
|
|
309
309
|
path: op.path.replace('$', '$.data'),
|
|
310
310
|
}));
|
|
311
311
|
|
|
312
312
|
// Group by operation type
|
|
313
|
-
const groups =
|
|
314
|
-
// groups => { replace:
|
|
313
|
+
const groups = atomGroupBy(atom, (op) => op.op);
|
|
314
|
+
// groups => { replace: IJsonAtom, add: IJsonAtom, remove: IJsonAtom }
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
-
#### `operationSpecDict` / `
|
|
317
|
+
#### `operationSpecDict` / `atomSpecDict` -- Spec Introspection
|
|
318
318
|
|
|
319
319
|
Separate spec-defined fields from extension properties:
|
|
320
320
|
|
|
321
321
|
```typescript
|
|
322
|
-
import { operationSpecDict, operationExtensions,
|
|
322
|
+
import { operationSpecDict, operationExtensions, atomSpecDict } from 'json-diff-ts';
|
|
323
323
|
|
|
324
324
|
const op = { op: 'replace', path: '$.name', value: 'Bob', x_author: 'system' };
|
|
325
325
|
operationSpecDict(op); // { op: 'replace', path: '$.name', value: 'Bob' }
|
|
326
326
|
operationExtensions(op); // { x_author: 'system' }
|
|
327
327
|
|
|
328
|
-
// Strip all extensions from
|
|
329
|
-
const clean =
|
|
328
|
+
// Strip all extensions from an atom
|
|
329
|
+
const clean = atomSpecDict(atom);
|
|
330
330
|
```
|
|
331
331
|
|
|
332
332
|
#### `leafProperty` -- Path Introspection
|
|
@@ -385,15 +385,15 @@ const all = comparisonToFlatList(result, { includeUnchanged: true });
|
|
|
385
385
|
|
|
386
386
|
### Audit Log
|
|
387
387
|
|
|
388
|
-
Store every change to a document as a reversible
|
|
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
389
|
|
|
390
390
|
```typescript
|
|
391
|
-
import {
|
|
391
|
+
import { diffAtom, applyAtom, revertAtom, IJsonAtom } from 'json-diff-ts';
|
|
392
392
|
|
|
393
393
|
interface AuditEntry {
|
|
394
394
|
timestamp: string;
|
|
395
395
|
userId: string;
|
|
396
|
-
|
|
396
|
+
atom: IJsonAtom;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
const auditLog: AuditEntry[] = [];
|
|
@@ -407,13 +407,13 @@ let doc = {
|
|
|
407
407
|
};
|
|
408
408
|
|
|
409
409
|
function updateDocument(newDoc: typeof doc, userId: string) {
|
|
410
|
-
const
|
|
410
|
+
const atom = diffAtom(doc, newDoc, {
|
|
411
411
|
arrayIdentityKeys: { items: 'id' },
|
|
412
412
|
});
|
|
413
413
|
|
|
414
|
-
if (
|
|
415
|
-
auditLog.push({ timestamp: new Date().toISOString(), userId,
|
|
416
|
-
doc =
|
|
414
|
+
if (atom.operations.length > 0) {
|
|
415
|
+
auditLog.push({ timestamp: new Date().toISOString(), userId, atom });
|
|
416
|
+
doc = applyAtom(structuredClone(doc), atom);
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
return doc;
|
|
@@ -423,7 +423,7 @@ function updateDocument(newDoc: typeof doc, userId: string) {
|
|
|
423
423
|
function undo(): typeof doc {
|
|
424
424
|
const last = auditLog.pop();
|
|
425
425
|
if (!last) return doc;
|
|
426
|
-
doc =
|
|
426
|
+
doc = revertAtom(structuredClone(doc), last.atom);
|
|
427
427
|
return doc;
|
|
428
428
|
}
|
|
429
429
|
|
|
@@ -432,50 +432,50 @@ updateDocument(
|
|
|
432
432
|
{ ...doc, status: 'active', items: [{ id: 1, task: 'Design', done: true }, ...doc.items.slice(1)] },
|
|
433
433
|
'alice'
|
|
434
434
|
);
|
|
435
|
-
// auditLog[0].
|
|
435
|
+
// auditLog[0].atom.operations =>
|
|
436
436
|
// [
|
|
437
437
|
// { op: 'replace', path: '$.status', value: 'active', oldValue: 'draft' },
|
|
438
438
|
// { op: 'replace', path: '$.items[?(@.id==1)].done', value: true, oldValue: false }
|
|
439
439
|
// ]
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
-
Because every
|
|
442
|
+
Because every atom is self-describing JSON, your audit log is queryable, storable in any database, and readable from any language.
|
|
443
443
|
|
|
444
444
|
### Undo / Redo Stack
|
|
445
445
|
|
|
446
|
-
Build undo/redo for any JSON state object.
|
|
446
|
+
Build undo/redo for any JSON state object. Atoms are small (only changed fields), reversible, and serializable:
|
|
447
447
|
|
|
448
448
|
```typescript
|
|
449
|
-
import {
|
|
449
|
+
import { diffAtom, applyAtom, revertAtom, IJsonAtom } from 'json-diff-ts';
|
|
450
450
|
|
|
451
451
|
class UndoManager<T extends object> {
|
|
452
|
-
private undoStack:
|
|
453
|
-
private redoStack:
|
|
452
|
+
private undoStack: IJsonAtom[] = [];
|
|
453
|
+
private redoStack: IJsonAtom[] = [];
|
|
454
454
|
|
|
455
455
|
constructor(private state: T) {}
|
|
456
456
|
|
|
457
457
|
apply(newState: T): T {
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
460
|
-
this.undoStack.push(
|
|
458
|
+
const atom = diffAtom(this.state, newState);
|
|
459
|
+
if (atom.operations.length === 0) return this.state;
|
|
460
|
+
this.undoStack.push(atom);
|
|
461
461
|
this.redoStack = [];
|
|
462
|
-
this.state =
|
|
462
|
+
this.state = applyAtom(structuredClone(this.state), atom);
|
|
463
463
|
return this.state;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
undo(): T {
|
|
467
|
-
const
|
|
468
|
-
if (!
|
|
469
|
-
this.redoStack.push(
|
|
470
|
-
this.state =
|
|
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
471
|
return this.state;
|
|
472
472
|
}
|
|
473
473
|
|
|
474
474
|
redo(): T {
|
|
475
|
-
const
|
|
476
|
-
if (!
|
|
477
|
-
this.undoStack.push(
|
|
478
|
-
this.state =
|
|
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
479
|
return this.state;
|
|
480
480
|
}
|
|
481
481
|
}
|
|
@@ -483,55 +483,55 @@ class UndoManager<T extends object> {
|
|
|
483
483
|
|
|
484
484
|
### Data Synchronization
|
|
485
485
|
|
|
486
|
-
Send only what changed between client and server.
|
|
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
487
|
|
|
488
488
|
```typescript
|
|
489
|
-
import {
|
|
489
|
+
import { diffAtom, applyAtom, validateAtom } from 'json-diff-ts';
|
|
490
490
|
|
|
491
|
-
// Client side: compute and send
|
|
492
|
-
const
|
|
491
|
+
// Client side: compute and send atom
|
|
492
|
+
const atom = diffAtom(localState, updatedState, {
|
|
493
493
|
arrayIdentityKeys: { records: 'id' },
|
|
494
494
|
});
|
|
495
495
|
await fetch('/api/sync', {
|
|
496
496
|
method: 'POST',
|
|
497
|
-
body: JSON.stringify(
|
|
497
|
+
body: JSON.stringify(atom),
|
|
498
498
|
});
|
|
499
499
|
|
|
500
500
|
// Server side: validate and apply
|
|
501
|
-
const result =
|
|
501
|
+
const result = validateAtom(req.body);
|
|
502
502
|
if (!result.valid) return res.status(400).json(result.errors);
|
|
503
503
|
// ⚠️ In production, sanitize paths/values to prevent prototype pollution
|
|
504
504
|
// (e.g. reject paths containing "__proto__" or "constructor")
|
|
505
|
-
currentState =
|
|
505
|
+
currentState = applyAtom(structuredClone(currentState), req.body);
|
|
506
506
|
```
|
|
507
507
|
|
|
508
508
|
---
|
|
509
509
|
|
|
510
|
-
## Bridge: v4 Changeset <-> JSON
|
|
510
|
+
## Bridge: v4 Changeset <-> JSON Atom
|
|
511
511
|
|
|
512
|
-
Convert between the legacy internal format and JSON
|
|
512
|
+
Convert between the legacy internal format and JSON Atom:
|
|
513
513
|
|
|
514
514
|
```typescript
|
|
515
|
-
import { diff,
|
|
515
|
+
import { diff, toAtom, fromAtom, unatomizeChangeset } from 'json-diff-ts';
|
|
516
516
|
|
|
517
|
-
// v4 changeset → JSON
|
|
517
|
+
// v4 changeset → JSON Atom
|
|
518
518
|
const changeset = diff(source, target, { arrayIdentityKeys: { items: 'id' } });
|
|
519
|
-
const
|
|
519
|
+
const atom = toAtom(changeset);
|
|
520
520
|
|
|
521
|
-
// JSON
|
|
522
|
-
const atoms =
|
|
521
|
+
// JSON Atom → v4 atomic changes
|
|
522
|
+
const atoms = fromAtom(atom);
|
|
523
523
|
|
|
524
524
|
// v4 atomic changes → hierarchical changeset (if needed)
|
|
525
525
|
const cs = unatomizeChangeset(atoms);
|
|
526
526
|
```
|
|
527
527
|
|
|
528
|
-
**Note:** `
|
|
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.
|
|
529
529
|
|
|
530
530
|
---
|
|
531
531
|
|
|
532
532
|
## Legacy Changeset API (v4 Compatibility)
|
|
533
533
|
|
|
534
|
-
All v4 APIs remain fully supported. Existing code continues to work without changes. For new projects, prefer the JSON
|
|
534
|
+
All v4 APIs remain fully supported. Existing code continues to work without changes. For new projects, prefer the JSON Atom API above.
|
|
535
535
|
|
|
536
536
|
### `diff`
|
|
537
537
|
|
|
@@ -659,8 +659,8 @@ interface Options {
|
|
|
659
659
|
## Migration from v4
|
|
660
660
|
|
|
661
661
|
1. **No action required** -- all v4 APIs work identically in v5.
|
|
662
|
-
2. **Adopt JSON
|
|
663
|
-
3. **Bridge existing data** -- `
|
|
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
664
|
4. **Rename `embeddedObjKeys` to `arrayIdentityKeys`** -- the old name still works, but `arrayIdentityKeys` is the preferred name going forward.
|
|
665
665
|
5. Both formats coexist. No forced migration.
|
|
666
666
|
|
|
@@ -675,13 +675,13 @@ interface Options {
|
|
|
675
675
|
| Dependencies | Zero | Few | Many | Varies |
|
|
676
676
|
| ESM Support | Native | CJS only | CJS only | Varies |
|
|
677
677
|
| Array Identity | Key-based | Index only | Configurable | Index only |
|
|
678
|
-
| Wire Format | JSON
|
|
678
|
+
| Wire Format | JSON Atom (standardized) | Proprietary | Proprietary | JSON Pointer |
|
|
679
679
|
| Reversibility | Built-in (`oldValue`) | Manual | Plugin | Not built-in |
|
|
680
680
|
|
|
681
681
|
## FAQ
|
|
682
682
|
|
|
683
|
-
**Q: How does JSON
|
|
684
|
-
JSON Patch uses JSON Pointer (`/items/0`) for paths, which breaks when array elements are inserted, deleted, or reordered. JSON
|
|
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
685
|
|
|
686
686
|
**Q: Can I use this with React / Vue / Angular?**
|
|
687
687
|
Yes. json-diff-ts works in any JavaScript runtime -- browsers, Node.js, Deno, Bun, edge workers.
|
|
@@ -689,8 +689,8 @@ Yes. json-diff-ts works in any JavaScript runtime -- browsers, Node.js, Deno, Bu
|
|
|
689
689
|
**Q: Is it suitable for large objects?**
|
|
690
690
|
Yes. The library handles large, deeply nested JSON structures efficiently with zero dependencies and a ~6KB gzipped footprint.
|
|
691
691
|
|
|
692
|
-
**Q: Can I use the v4 API alongside JSON
|
|
693
|
-
Yes. Both APIs coexist. Use `
|
|
692
|
+
**Q: Can I use the v4 API alongside JSON Atom?**
|
|
693
|
+
Yes. Both APIs coexist. Use `toAtom()` / `fromAtom()` to convert between formats.
|
|
694
694
|
|
|
695
695
|
**Q: What about arrays of primitives?**
|
|
696
696
|
Use `$value` as the identity key: `{ arrayIdentityKeys: { tags: '$value' } }`. Elements are matched by value identity.
|
|
@@ -700,14 +700,14 @@ Use `$value` as the identity key: `{ arrayIdentityKeys: { tags: '$value' } }`. E
|
|
|
700
700
|
## Release Notes
|
|
701
701
|
|
|
702
702
|
- **v5.0.0-alpha.2:**
|
|
703
|
-
-
|
|
704
|
-
-
|
|
703
|
+
- Atom workflow helpers: `squashAtoms`, `atomMap`, `atomStamp`, `atomGroupBy`
|
|
704
|
+
- Atom/operation introspection: `operationSpecDict`, `operationExtensions`, `atomSpecDict`, `atomExtensions`, `leafProperty`
|
|
705
705
|
- Comparison serialization: `comparisonToDict`, `comparisonToFlatList`
|
|
706
706
|
|
|
707
707
|
- **v5.0.0-alpha.0:**
|
|
708
|
-
- JSON
|
|
708
|
+
- JSON Atom API: `diffAtom`, `applyAtom`, `revertAtom`, `invertAtom`, `toAtom`, `fromAtom`, `validateAtom`
|
|
709
709
|
- Canonical path production with typed filter literals
|
|
710
|
-
- Conformance with the [JSON
|
|
710
|
+
- Conformance with the [JSON Atom Specification](https://github.com/ltwlf/json-atom-format) v0
|
|
711
711
|
- Renamed `embeddedObjKeys` to `arrayIdentityKeys` (old name still works as deprecated alias)
|
|
712
712
|
- All v4 APIs preserved unchanged
|
|
713
713
|
|