Object diff and patch
npm install wson-diff
> A differ/patcher for arbitrary values that presents delta in a terse WSON-like format.
WSON can be used to stringify structured data, transmit that string to some receiver, where it can be parsed to reconstruct that original data. Now both ends posses that identical data. If now that data happens to change a little, why should we retransmit that whole redundant information? This is where wson-diff comes in:
1. Generate a delta by either:
- Call diff with your old value have and the current wish.
- Manually build up this delta.
2. Send the delta over the wire.
3. On the receiver end: Call patch to apply that delta to the value have in being.
diff uses mdiff to find minimal changes of arrays and strings. For arrays this changes are further boiled down to deletes, moves, inserts and replaces.patch can use notifiers to forward changes to some dependent (DOM?)-structure.``bash`
$ npm install wson-diff
`js
import wdiffFactory from 'wson-diff'
const wdiff = wdiffFactory()
var have = {
name: 'otto',
size: 177.3,
completed: ['forth', 'javascript', 'c++', 'haskell'],
active: true,
message: 'My hovercraft is full of eels.'
}
var wish = {
name: 'rudi',
size: 177.4,
completed: ['forth', 'coffeescript', 'haskell', 'c++', 'lisp'],
active: false,
message: 'My hovercraft is full of eels!'
}
var delta = wdiff.diff(have, wish)
console.log('delta=\'%s\'', delta)
// delta='|active:#f|completed[m3@2][i4:lisp][r1:coffeescript]|message[s29=!]|name:rudi|size:#177.4'
var result = wdiff.patch(have, delta)
console.log('result=\'%j\'', result)
// Now result (and have) is deep equal to wish.
`
This is an informal description of the Delta-Syntax. There is also an EBNF-file in the doc-directory and a syntax-diagram created from it.
If have is deep equal to wish, diff returns null.
Examples:
| have | wish |23
|---------------------|---------------------|
| | 23 |{a: 3, b: 4}
| | {a: 3, b: 4} |[3, 4]
| | [3, 4] |'hovercraft'
| | 'hovercraft' |
Any WSON-value is a plain-delta. Its semantics is: Ignore have, just use the WSON-stringified delta as the result.
A plain-delta will be produced for scalars (everything but string, array, or object – except Date, which _are_ WSON-scalars) and if have and wish have different types.
Examples:
| have | wish | delta |23
|---------------------|---------------------|------------------|
| | 42 | #42 |false
| | true | #t |undefined
| | null | #n |[3, 4]
| | {a: 3, b: 4} | {a:3\|b:4} |{a: 3, b: 4}
| | [3, 4] | [3\|4] |
A real-delta starts with a '|' followed by zero ore more modifiers followed by zero ore more assignments. The assignments are separated from each other by '|'. There must be at least one modifier or assignments. Since the only WSON-value that starts with a '|' is a backref, which can't occur at top-level, there is no ambiguity.
A path consists of one ore more keys separated by '|'. Each key is a WSON-string.
Examples:
| path | keys |
|--------------------------|---------------------------------------------------------------------------------------------------|
| a | 'a' |
| foo\|a\|members | 'foo', 'a', 'members' |
| foo\|\a\e\|#\|42 | 'foo', '[]', '', '42' (Special characters '[' and ']' are WSON-escaped; '#' is the empty string.) |
An assignment consists of a path followed by a ':', followed by a WSON-value. Its semantics is: Use path to dive into the object (All keys but the last must resolve to objects). Use the last key to set or replace that property by value.
Examples:
| have | wish | delta |{a: 3, b: 4}
|------------------------|-------------------------------|------------------|
| | {a: 3, b: 42} | \|b:#42 |{foo: {a: 3, b: 4}}
| | {foo: {a: 3, b: 42}} | \|foo\|b:#42 |{foo: {a: 3, b: 4}}
| | {foo: {a: 3, b: 4, c: 5}} | \|foo\|c:#5 |
A Modifier consists of a '[', followed by a kind character, followed by one or more '|'-separated kind-specific items, closed by a ']'.
A path-delta is an assignments or a path followed by one ore more modifiers.
There are two modifiers that operate on an object: unset, and assign. Deltas of objects are created by deep comparing all own properties of these objects. See also: Custom Objects
- kind character: '-'
- item: key, a WSON-string
- Semantics: Remove key from object
Examples:
| have | wish | delta |{a: 3, b: 4, c: 5}
|-----------------------|---------------------|------------------|
| | {b: 4} | [-a\|c] |
- kind character: '='
- item: a path-delta
- Semantics: Set, replace or modify the referred value.
Examples:
| have | wish | delta |{foo: {a: 3, b: 4, c: 5}}
|------------------------------|-----------------------------|--------------------|
| | {foo: {a: 4, b: 3, c: 5}} | \|foo[=a:#4\|b:#3] |
There are four modifiers that operate on an array: delete, move insert, and replace.
Since arrays are assumed to be mutable, these modifiers work cumulatively. I.e. the indexes refer to the array after all previous modifications applied):
Note: diff will create a plain-delta:arrayLimit
- for arrays that differ too much (more then entry changes).
- kind character: 'd'
- item: an index optionally followed by '+' and extra-count
- Semantics: Remove one or more entries from the array. If extra-count is specified, then (extra-count + 1) entries will be deleted.
Examples:
| have | wish | delta | Explanation |[2, 3, 5, 7, 11, 13]
|-------------------------|-------------------------|------------------|---------------------------------------------------------------------------------------------|
| | [2, 5, 7, 11, 13] | [d1] | Delete one entry at index 1 |[2, 3, 5, 7, 11, 13]
| | [3, 5, 13] | [d3+1\|0] | Delete 2 entries (one + 1) at index 3. From the resulting array delete one entry at index 0 |
- kind character: 'm'
- item: an source-index, optionally followed by '+' or '-' and an extra-count, followed by '@' and an destination-index.
- Semantics: Move one or more entries in the array. If extra-count is specified, then (extra-count + 1) entries will be moved. The sequence to move will be first cut out at source-index and then reinserted at destination-index (which applies to the already reduced array). If there is a '-', the sequence will be reversed before reinsertion.
Examples:
| have | wish | delta | Explanation |[2, 3, 5, 7, 11, 13]
|-------------------------|---------------------------|---------------------|-------------------------------------------------------------------------|
| | [2, 3, 7, 11, 5, 13] | [m2@4] | Cut one entry at index 2 and reinsert it at index 4 |[2, 3, 5, 7, 11, 13]
| | [2, 11, 13, 3, 5, 7] | [m4+1@1] | Cut 2 entries (one + 1) at index 4 and reinsert them at index 1 |[2, 3, 5, 7, 11, 13]
| | [2, 13, 11, 3, 5, 7] | [m4-1@1] | Cut 2 entries (one + 1) at index 4 and reinsert them swapped at index 1 |
- kind character: 'i'
- item: an index followed by one or more WSON-values, each prefixed by a ':'
- Semantics: Insert one or more entries into the array.
Examples:
| have | wish | delta | Explanation |[2, 5, 7, 11, 13]
|-------------------------|-------------------------|-------------------|--------------------------------------------------------------------------------------------|
| | [2, 3, 5, 7, 11, 13] | [i1:#3] | Insert value 3 at index 1. |[3, 5, 13]
| | [2, 3, 5, 7, 11, 13] | [i2:#7:#11\|0:#2] | Insert values 7, 11 at index 11. Into the resulting array insert value 2 at index 0. |
- kind character: 'r'
- item: an index followed by either:
- one or more WSON-strings, each prefixed by a ':'
- a single path modifier, prefixed by a '|'
- Semantics: Replace one or more entries of the array.
Examples:
| have | wish | delta | Explanation |[2, 3, 5, 7, 11, 13]
|-------------------------|---------------------------|---------------------|---------------------------------------------------------------------------------------------|
| | [2, 3, 15, 7, 11, 13] | [r2:#15] | Replace the entry at index 2 with 15. |[2, 3, 5, 7, 11, 13]
| | [2, 23, 15, 7, 1, 13] | [r1:#23:#15\|4:#1] | Replace the entries at index 1 with 23, 15. Then replace the entry at index 4 with 1. |[{a: 3}, {b: 4}]
| | [{a: 4}, {b: 3}] | [r0\|a:#4\|1\|b:#3] | Replace 'a' at index 0, then 'b' at index 1. |
---
diff will produce array-modifiers in the order delete, move, insert, replace
Examples:
| have | wish | delta | Explanation[2, 3, 5, 7, 11, 13]
|-------------------------|---------------------------|------------------------|------------------------------------------------|
| | [5, 11, 13, 7, 42] | [d0+1][m1@3][i4:#42] | Delete 2, 3, move 7, insert 42. |[2, 3, 5, 7, 11, 13]
| | [13, 11, 2, 3, 51, 7] | [m4-1@0][r4:#51] | Move reversed 11, 13, replace 5 by 51. |
There in one modifier that operates on a string: substitute.
Since strings are assumed to be immutable, these modifiers work simultaneously (in contrast to array-modifiers:
Note: diff will create a plain-delta:stringEdge
- for short strings (length < )stringLimit
- for strings that differ too much (more then character changes).
- kind character: 's'
- item: an index optionally followed by '+', '-' and length-modifier number, then optionally a '=' and a non empty WSON-escaped replacement-string.
- Semantics: Replace the substring at the specified index with the string after the '='. The replaced substring grow ('+') or shrink ('-') by the length modifier.
I.e. a missing length-modifier results in a pure replacement.
Examples (with stringEdge: 0):
| have | wish | delta | Explanation |'hovercraft'
|---------------------------|---------------------------|---------------------|--------------------|
| | 'Hovercraft' | [s0=H] | simple replacement |'my hovercraft'
| | 'thine hovercraft' | [s0+3=thine] | grow replacement |'hovercraft is missing'
| | 'hovercraft is away' | [s14-3=away] | shrink replacement |'full of my eels'
| | 'full of eels' | [s8-3] | pure deletion |'my hovercraft'
| | 'hover my craft' | [s0-3\|8+4= my ] | delete and insert |
The underlying WSON-processor may be created with connectors to support custom objects. These conectectors can be augmented for wson-diff with these extra properties:diffKeys
- (array of strings): Instead of looking at all own properties diff just compares the properties referred by these keys. Thus you can hide private properties from delta.postpatch
- (function): This function - if present - will be called (bound to the current object) after all patches have been applied. Thus you can update private properties thereafter.
---
#### Complex Examples
| have | wish | delta | Explanation |
|-----------------------------|-----------------------------|--------------------------|--------------------|
| {foo: {a: [1, 2]}} | {foo: {a: [1, 2, 3]}} | \|foo\|a[i2:#3] | |
| {foo: {a: [1, 2]}} | {bar: {a: [1, 2]}} | \|[-foo]bar:{a:[#1\|#2]} | sorry, no renaming |
| {foo: {a: [1, 2]}} | {foo: {a: 1, b: 1}} | \|foo[=a:#1\|b:#1] | assign modifier |
| [{a: 'alice'}, {b: 'bob'}] | [{a: 'eve'}, {b: 'bob'}] | \|[r0\|a:eve] | |
Be that you are not only interested in in the result of patching some value by a delta, but want to update some related structure - say a DOM-tree - accordingly. This task can be accomplished by passing patchone or notifiers, that should provide the following interface:
`ts`
interface Notifier {
checkedBudge: (up: number, key: Key, current: any) => boolean
unset: (key: string, curent: any) => void
assign: (key: string | null, value: any, current?: any) => void
delete: (idx: number, len: number, current?: any) => void
move: (srcIdx: number, dstIdx: number, len: number, reverse: boolean, current?: any) => void
insert: (idx: number, values: any[], current?: any) => void
replace: (idx: number, values: any[], current?: any) => void
substitute: (patches: Patch[], current?: any) => void
}checkedBudge
A notifier is assumed to to hold some cursor that could be manipulated by calling : up is a number >= 0 that says how many levels we want to go up. If key is not null it is a numeric array-index or a string object-key. Then the cursor should be moved into the item that is indicated by key. If a key is provided (or for a first extra call with up=0, key=null), checkedBudge may return false to signal a cut. Inititally the cursor refers to the root-value (The have that is passed to patch).
The other methods just resemble the parsed modifiers. For convenience the current value (that one the cursor refers to, before apllying the modification) is passed as an additional argument current.
assign may be called with or without key. If key is null, the item under the cursor is expected to be replaced. Otherwise the item referred by this key is expected to be set or replaced.
To finer tailor the amount of notification checkedBudge may return false. Then all modifications for the value reached by key will be collected and notified by a single call of assign (without a current argument). E.g. if checkedBudge returns false whenever current happens to be a string, a substitute-delta will never have substitute be called but results in the same call of assign as if there had been a plain-delta for this string.
#### var wdiff = wson-diff(options)
Creates a new diff/patch processor.
Recognized options:
- WSON: a WSON-processor.wsonOptions
- : If no WSON is provided, create one with this options.
Other options are handed to diff.
#### var delta = wdiff.diff(have, wish, options)
Returns null if have and wish are deep equal. Otherwise returns the string delta.
Recognized options:
- arrayLimit (integer or function, default: null): If the number of differences between some have-array and some wish-array exceeds this limit, a plain-delta will be created instead of a list of modifiers. A move will count as twice it's length. This is to limit the amount of time used to find the minimal set of changes by the underlying mdiff. If arrayLimit is a function, it will applied to (have-array, wish-array) to return the limit dynamically.
- stringLimit (integer or function, default: null): If the number of differences between some have-string and some wish-string exceeds this limit, a plain-delta will be created instead of a substitute-modifier. Every deleted or inserted character count as one change. If stringLimit is a function, it will applied to (have-string, wish-string) to return the limit dynamically.
- stringEdge (integer, default: 16): If some wish-string is shorter than this limit, a plain-delta will be created instead of a substitute-modifier.
#### var result = wdiff.patch(have, delta, options)
Returns the result of applying delta to have. If possible, have will be changed in place.
Recognized options:
- notifiers`: an array of notifiers, each of which receives all applied changes. A single notifier is accepted, too.