-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ability to diff/patch states. Closes #36 #37
Changes from 6 commits
a84d34e
af486ca
434f133
11d4552
2b3fa60
59d174e
dc0133a
ddbce4f
e0ee322
3d3ee43
dae1c6c
6716aaa
839f31c
370e8bc
720b94f
b3a8460
002fe8d
f76a750
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -167,25 +167,85 @@ define([ | |
return json; | ||
} | ||
|
||
async apply (node, state, resolvedSelectors=new NodeSelections()) { | ||
await this.resolveSelectors(node, state, resolvedSelectors); | ||
async apply(node, state, resolvedSelectors = new NodeSelections()) { | ||
const diffs = await this.getDiffs(node, state, resolvedSelectors); | ||
await this.applyDiffs(diffs, resolvedSelectors); | ||
} | ||
|
||
async getDiffs(node, state, resolvedSelectors=new NodeSelections()) { | ||
await this.resolveSelectorsForExistingNodes(node, state, resolvedSelectors); | ||
|
||
const parent = this.core.getParent(node); | ||
const parentPath = this.core.getPath(parent) || ''; | ||
const nodePath = this.core.getPath(node); | ||
const diffs = []; | ||
const children = state.children || []; | ||
const currentChildren = await this.core.loadChildren(node); | ||
|
||
for (let i = 0; i < children.length; i++) { | ||
const idString = children[i].id; | ||
const child = await this.findNode(node, idString, resolvedSelectors); | ||
const index = currentChildren.indexOf(child); | ||
diffs.push(...(await Promise.all(children.map(async childState => { | ||
const idString = childState.id; | ||
const childNode = await this.findNode(node, idString, resolvedSelectors); | ||
const index = currentChildren.indexOf(childNode); | ||
if (index > -1) { | ||
currentChildren.splice(index, 1); | ||
} | ||
if (childNode) { | ||
const childDiffs = await this.getDiffs(childNode, childState, resolvedSelectors); | ||
return childDiffs; | ||
} else { | ||
return [ | ||
new NodeChangeSet( | ||
nodePath, | ||
childState.id || '', | ||
'add_subtree', | ||
'', | ||
childState | ||
) | ||
]; | ||
} | ||
}))).flat()); | ||
const current = await this.toJSON(node, new OmittedProperties(['children'])); | ||
const changes = this._getSortedStateChanges(current, state); | ||
if(changes.length) { | ||
diffs.push(...changes.map( | ||
change => NodeChangeSet.fromDiffObj( | ||
parentPath, | ||
nodePath, | ||
change | ||
) | ||
)); | ||
} | ||
|
||
await this.apply(child, children[i], resolvedSelectors); | ||
if(state.children && currentChildren.length) { | ||
const deletions = currentChildren.map(child => new NodeChangeSet( | ||
nodePath, | ||
this.core.getPath(child), | ||
'remove_subtree', | ||
this.core.getPath(child), | ||
null | ||
)); | ||
diffs.push(...deletions); | ||
} | ||
|
||
const current = await this.toJSON(node); | ||
const changes = compare(current, state); | ||
return diffs; | ||
} | ||
|
||
async applyDiffsForNode(node, diffs) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rename this to |
||
const resolvedSelectors = new NodeSelections(); | ||
const currentState = await this.toJSON(node); | ||
await this.resolveSelectors(node, currentState, resolvedSelectors); | ||
brollb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await this.applyDiffs(diffs, resolvedSelectors); | ||
brollb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
async applyDiffs(diffs, resolvedSelectors) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would name this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, this won't work if we want to support the lazy creation of subtrees (in which it diffs the subtree when the node is created). |
||
await Promise.all(diffs.map(async diff => { | ||
const parent = await this.core.loadByPath(this.rootNode, diff.parentPath); | ||
const node = await this.findNode(parent, diff.nodeId, resolvedSelectors); | ||
return await this.applyDiffs[diff.type].call(this, node, diff, resolvedSelectors); | ||
})); | ||
} | ||
|
||
_getSortedStateChanges(prevState, newState) { | ||
const keyOrder = [ | ||
'children_meta', | ||
'pointer_meta', | ||
|
@@ -195,12 +255,13 @@ define([ | |
'member_attributes', | ||
'member_registry', | ||
]; | ||
|
||
const changes = compare(prevState, newState); | ||
const singleKeyFields = ['children_meta', 'guid']; | ||
const sortedChanges = changes | ||
.filter( | ||
change => change.key.length > 1 || | ||
(singleKeyFields.includes(change.key[0]) && change.type === 'put') | ||
) | ||
const sortedChanges = changes.filter( | ||
change => change.key.length > 1 || | ||
(singleKeyFields.includes(change.key[0]) && change.type === 'put') | ||
) | ||
.map((change, index) => { | ||
let order = 2 * keyOrder.indexOf(change.key[0]); | ||
if (change.type === 'put') { | ||
|
@@ -210,20 +271,7 @@ define([ | |
}) | ||
.sort((p1, p2) => p1[0] - p2[0]) | ||
.map(pair => changes[pair[1]]); | ||
|
||
for (let i = 0; i < sortedChanges.length; i++) { | ||
if (sortedChanges[i].type === 'put') { | ||
await this._put(node, sortedChanges[i], resolvedSelectors); | ||
} else if (sortedChanges[i].type === 'del') { | ||
await this._delete(node, sortedChanges[i], resolvedSelectors); | ||
} | ||
} | ||
|
||
if (state.children) { | ||
for (let i = currentChildren.length; i--;) { | ||
this.core.deleteNode(currentChildren[i]); | ||
} | ||
} | ||
return sortedChanges; | ||
} | ||
|
||
async resolveSelector(node, state, resolvedSelectors) { | ||
|
@@ -247,15 +295,15 @@ define([ | |
return (state.children || []).map(s => [s, node]); | ||
} | ||
|
||
async tryResolveSelectors(stateNodePairs, resolvedSelectors) { | ||
async tryResolveSelectors(stateNodePairs, resolvedSelectors, create) { | ||
let tryResolveMore = true; | ||
while (tryResolveMore) { | ||
tryResolveMore = false; | ||
for (let i = stateNodePairs.length; i--;) { | ||
const [state, parentNode] = stateNodePairs[i]; | ||
let child = await this.findNode(parentNode, state.id, resolvedSelectors); | ||
//const canCreate = !state.id; | ||
if (!child /*&& canCreate*/) { | ||
if (!child && create) { | ||
let baseNode; | ||
if (state.pointers) { | ||
const {base} = state.pointers; | ||
|
@@ -275,13 +323,13 @@ define([ | |
child = await this.createNode(parentNode, state, baseNode); | ||
} | ||
} | ||
|
||
let pairs = []; | ||
if (child) { | ||
this.resolveSelector(child, state, resolvedSelectors); | ||
const pairs = this.getChildStateNodePairs(child, state); | ||
stateNodePairs.splice(i, 1, ...pairs); | ||
this.resolveSelector(child, state, resolvedSelectors, create); | ||
pairs = this.getChildStateNodePairs(child, state); | ||
tryResolveMore = true; | ||
} | ||
stateNodePairs.splice(i, 1, ...pairs); | ||
} | ||
} | ||
|
||
|
@@ -290,15 +338,19 @@ define([ | |
} | ||
} | ||
|
||
async resolveSelectors(node, state, resolvedSelectors) { | ||
async resolveSelectorsForExistingNodes(node, state, resolvedSelectors) { | ||
await this.resolveSelectors(node, state, resolvedSelectors, false); | ||
} | ||
|
||
async resolveSelectors(node, state, resolvedSelectors, create=true) { | ||
const parent = this.core.getParent(node); | ||
|
||
if (state.id && parent) { | ||
this.resolveSelector(node, state, resolvedSelectors); | ||
} | ||
|
||
const stateNodePairs = this.getChildStateNodePairs(node, state); | ||
await this.tryResolveSelectors(stateNodePairs, resolvedSelectors); | ||
await this.tryResolveSelectors(stateNodePairs, resolvedSelectors, create); | ||
} | ||
|
||
async findNode(parent, idString, resolvedSelectors=new NodeSelections()) { | ||
|
@@ -351,6 +403,25 @@ define([ | |
return node; | ||
} | ||
|
||
async createStateSubTree(parentPath, state, resolvedSelectors) { | ||
const base = state.pointers?.base; | ||
const parent = await this.core.loadByPath(this.rootNode, parentPath); | ||
const baseNode = await this.findNode(parent, base, resolvedSelectors); | ||
const created = await this.createNode(parent, state, baseNode); | ||
const nodeSelector = new NodeSelector(this.core.getPath(created)); | ||
resolvedSelectors.record(parentPath, nodeSelector, created); | ||
const nodeState = await this.toJSON(created, new OmittedProperties(['children'])); | ||
const changes = this._getSortedStateChanges(nodeState, state); | ||
await Promise.all(changes.map(async change => { | ||
await this._put(created, change, resolvedSelectors); | ||
})); | ||
await Promise.all((state.children || []).map(async child => { | ||
await this.createStateSubTree(this.core.getPath(created), child, resolvedSelectors); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this might have problems with inherited children. I will put together a test and check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this is a problem in main, too, so probably not a deal breaker... |
||
})); | ||
|
||
return created; | ||
} | ||
|
||
async _put (node, change) { | ||
const [type] = change.key; | ||
if (type !== 'path' && type !== 'id') { | ||
|
@@ -378,6 +449,24 @@ define([ | |
} | ||
} | ||
|
||
Importer.prototype.applyDiffs.add_subtree = async function(node, change, resolvedSelectors) { | ||
const created = await this.createStateSubTree(change.parentPath, change.value, resolvedSelectors); | ||
return created; | ||
}; | ||
|
||
Importer.prototype.applyDiffs.remove_subtree = async function(node, /*change, resolvedSelectors*/) { | ||
this.core.deleteNode(node); | ||
}; | ||
|
||
Importer.prototype.applyDiffs.put = async function(node, change, resolvedSelectors) { | ||
return await this._put(node, change, resolvedSelectors); | ||
}; | ||
|
||
Importer.prototype.applyDiffs.del = async function(node, change, resolvedSelectors) { | ||
return await this._delete(node, change, resolvedSelectors); | ||
}; | ||
|
||
|
||
Importer.prototype._put.guid = async function(node, change, resolvedSelectors) { | ||
const {value} = change; | ||
this.core.setGuid(node, value); | ||
|
@@ -405,7 +494,6 @@ define([ | |
change.key.length === 2, | ||
`Complex attributes not currently supported: ${change.key.join(', ')}` | ||
); | ||
|
||
const [/*type*/, name] = change.key; | ||
this.core.setAttribute(node, name, change.value); | ||
}; | ||
|
@@ -732,6 +820,13 @@ define([ | |
return object; | ||
} | ||
|
||
function partition(array, fn) { | ||
return array.reduce((arr, next, index, records) => { | ||
arr[fn(next, index, records) ? 0 : 1].push(next); | ||
return arr; | ||
}, [[], []]); | ||
} | ||
|
||
class NodeSelector { | ||
constructor(idString='') { | ||
if (idString.startsWith('/')) { | ||
|
@@ -792,33 +887,13 @@ define([ | |
} | ||
|
||
if (this.tag === '@meta') { | ||
const metanodes = Object.values(core.getAllMetaNodes(rootNode)); | ||
const libraries = core.getLibraryNames(rootNode) | ||
.map(name => [ | ||
core.getPath(core.getLibraryRoot(rootNode, name)), | ||
name, | ||
]); | ||
|
||
function getFullyQualifiedName(node) { | ||
const name = core.getAttribute(node, 'name'); | ||
const path = core.getPath(node); | ||
const libraryPair = libraries.find(([rootPath,]) => path.startsWith(rootPath)); | ||
if (libraryPair) { | ||
const [,libraryName] = libraryPair; | ||
return libraryName + '.' + name; | ||
} | ||
return name; | ||
} | ||
|
||
return metanodes | ||
.find(child => { | ||
const name = core.getAttribute(child, 'name'); | ||
const fullName = getFullyQualifiedName(child); | ||
return name === this.value || fullName === this.value; | ||
}); | ||
return this.findMetaNodeForTag(core, rootNode); | ||
} | ||
|
||
if (this.tag === '@attribute') { | ||
if(!parent) { | ||
throw new Error(`cannot resolve tag ${this.tag} without a parent`); | ||
} | ||
const [attr, value] = this.value; | ||
const children = await core.loadChildren(parent); | ||
return children | ||
|
@@ -849,6 +924,33 @@ define([ | |
throw new Error(`Unknown tag: ${this.tag}`); | ||
} | ||
|
||
findMetaNodeForTag(core, rootNode) { | ||
const metanodes = Object.values(core.getAllMetaNodes(rootNode)); | ||
const libraries = core.getLibraryNames(rootNode) | ||
.map(name => [ | ||
core.getPath(core.getLibraryRoot(rootNode, name)), | ||
name, | ||
]); | ||
|
||
function getFullyQualifiedName(node) { | ||
const name = core.getAttribute(node, 'name'); | ||
const path = core.getPath(node); | ||
const libraryPair = libraries.find(([rootPath,]) => path.startsWith(rootPath)); | ||
if (libraryPair) { | ||
const [, libraryName] = libraryPair; | ||
return libraryName + '.' + name; | ||
} | ||
return name; | ||
} | ||
|
||
return metanodes | ||
.find(child => { | ||
const name = core.getAttribute(child, 'name'); | ||
const fullName = getFullyQualifiedName(child); | ||
return name === this.value || fullName === this.value; | ||
}); | ||
} | ||
|
||
async nodeSearch(core, node, fn, searchOpts = new NodeSearchOpts()) { | ||
if (searchOpts.cache && searchOpts.cacheKey) { | ||
const {cache, cacheKey} = searchOpts; | ||
|
@@ -1006,6 +1108,26 @@ define([ | |
} | ||
} | ||
|
||
class NodeChangeSet { | ||
constructor(parentPath, nodeId, type, key, value) { | ||
this.parentPath = parentPath; | ||
this.nodeId = nodeId; | ||
this.type = type; | ||
this.key = key; | ||
this.value = value; | ||
} | ||
|
||
static fromDiffObj(parentPath, nodeId, diffObj) { | ||
return new NodeChangeSet( | ||
parentPath, | ||
nodeId, | ||
diffObj.type, | ||
diffObj.key, | ||
diffObj.value | ||
); | ||
} | ||
} | ||
|
||
const RELATED_PROPERTIES = { | ||
sets: ['member_attributes', 'member_registry'], | ||
children: ['children_meta'], | ||
|
@@ -1036,5 +1158,7 @@ define([ | |
Importer.NodeSelector = NodeSelector; | ||
Importer.NodeSelections = NodeSelections; | ||
Importer.OmittedProperties = OmittedProperties; | ||
Importer.NodeChangeSet = NodeChangeSet; | ||
Importer.diff = compare; | ||
return Importer; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just call this
diff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we have
changesets.js
already nameddiff
. I am not sure, do we want to rename it to not create any confusion?