server/wraputil.js
function makeSyncOpGenericGetChangesBetween(handle, ws, rel, recurse, paramFuncs){

	//paramFuncs.forEach(function(pf){
	for(var i=0;i<paramFuncs.length;++i){
		var pf = paramFuncs[i]
		if(pf.isMacro){
			//_.errout('cannot apply this method if a param is a macro: ' + JSON.stringify(rel))
			return makeGenericGetChangesBetween(handle, ws, rel)
		}
	}
	//})
	//console.log(new Error().stack)
	function syncGenericGetChangesBetween(bindings, startEditId, endEditId, cb){
		_.assertLength(arguments, 4)
		
		if(startEditId === endEditId) _.errout('wasting time')
		
		var snaps = []
		
		var editIds = []
		var has = {}
		
		//console.log('getting ' + JSON.stringify(rel))
		
		var cdl = _.latch(paramFuncs.length, function(){
			if(editIds.length === 0){
				//console.log('no changes ' + startEditId + ' ' + endEditId)
				cb([])
				return
			}
			
			handle.getStateAt(bindings, startEditId, function(startState){
				handle.getStateAt(bindings, endEditId, function(state){
					if(startState === state){
						cb([])
					}else{
						var es = ws.diffFinder(startState, state)
						var changes = []
						es.forEach(function(e){
							e.editId = endEditId
							changes.push(e)
						})
						cb(changes)
					}
				})
			})
		})
		//console.log('here2')
		for(var i=0;i<paramFuncs.length;++i){
			paramFuncs[i].getChangesBetween(bindings, startEditId, endEditId, function(changes){
				changes.forEach(function(c){
					var editId = c.editId
					if(has[editId]) return
					has[editId] = true
					editIds.push(editId)
				})
						
				cdl()
			})
		}
	}
	return syncGenericGetChangesBetween
}
function makeSyncOpGenericGetChangesBetween(handle, ws, rel, recurse, paramFuncs){

	//paramFuncs.forEach(function(pf){
	for(var i=0;i<paramFuncs.length;++i){
		var pf = paramFuncs[i]
		if(pf.isMacro){
			//_.errout('cannot apply this method if a param is a macro: ' + JSON.stringify(rel))
			return makeGenericGetChangesBetween(handle, ws, rel)
		}
	}
	//})
	//console.log(new Error().stack)
	function syncGenericGetChangesBetween(bindings, startEditId, endEditId, cb){
		_.assertLength(arguments, 4)
		
		if(startEditId === endEditId) _.errout('wasting time')
		
		var snaps = []
		
		var editIds = []
		var has = {}
		
		//console.log('getting ' + JSON.stringify(rel))
		
		var cdl = _.latch(paramFuncs.length, function(){
			if(editIds.length === 0){
				//console.log('no changes ' + startEditId + ' ' + endEditId)
				cb([])
				return
			}
			
			handle.getStateAt(bindings, startEditId, function(startState){
				handle.getStateAt(bindings, endEditId, function(state){
					if(startState === state){
						cb([])
					}else{
						var es = ws.diffFinder(startState, state)
						var changes = []
						es.forEach(function(e){
							e.editId = endEditId
							changes.push(e)
						})
						cb(changes)
					}
				})
			})
		})
		//console.log('here2')
		for(var i=0;i<paramFuncs.length;++i){
			paramFuncs[i].getChangesBetween(bindings, startEditId, endEditId, function(changes){
				changes.forEach(function(c){
					var editId = c.editId
					if(has[editId]) return
					has[editId] = true
					editIds.push(editId)
				})
						
				cdl()
			})
		}
	}
	return syncGenericGetChangesBetween
}
function makeReallyGenericSyncOpGenericGetChangesBetween( find the union of the changed versions for the input parameters, use that to reduce the number of comparisons necessary