Last active
February 14, 2020 09:03
-
-
Save Kukks/283c4a2e5e18fc334328c83e114530a5 to your computer and use it in GitHub Desktop.
Aurelia Unit Of Work Observer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CloneUtility } from "./clone"; | |
import { Disposable } from "aurelia-binding"; | |
import { BindingEngine } from "aurelia-binding"; | |
/** | |
* Unit Of Work Observer | |
* Based on the work of fragsalat's MultiObserver found at | |
* https://gist.github.com/fragsalat/819a58021fc7b76a2704 | |
* | |
* | |
* The purpose of this observer is to observe all changes made to a model and allows you | |
* to efficiently send a list of modified/added/removed values | |
* This also supports keeping track of array children model! | |
*/ | |
export class Reflection { | |
public static getPropertyName( propertyFunction: Function ): string { | |
return /\.([^\.;]+);?\s*\}$/.exec(propertyFunction.toString())[1]; | |
} | |
public static getPropertiesOfObject( object: any ): string[] { | |
let result: string[] = []; | |
if ( typeof object === "object" ) { | |
for ( let i in object ) { | |
result.push(i); | |
} | |
} | |
return result; | |
} | |
} | |
export class DeepObserver implements Disposable { | |
private subscriptions: Disposable[] = []; | |
constructor( private bindingEngine: BindingEngine ) { | |
} | |
public observe( object: any, properties: string[], trackArrays: boolean, deepObserve: boolean, handler: Function ) { | |
// Map to correct values | |
if ( !properties || properties.length === 0 ) { | |
properties = Reflection.getPropertiesOfObject(object); | |
} | |
for ( let propertyName of properties ) { | |
// Determine the property name | |
let property = object ? propertyName : propertyName[1]; | |
// Determine the base object | |
let obj = object || propertyName[0]; | |
// Observe the array value or the property | |
if ( Array.isArray(obj[property]) && trackArrays ) { | |
this.subscriptions.push(this.bindingEngine.collectionObserver(obj[property]).subscribe(( changedValues: any ) => { | |
handler(propertyName, changedValues); | |
})); | |
} else { | |
if ( deepObserve ) { | |
const deeperProperties = Reflection.getPropertiesOfObject(obj[property]); | |
if ( deeperProperties && Array.isArray(deeperProperties) && deeperProperties.length > 0 ) { | |
this.observe(obj[property], deeperProperties, trackArrays, true, handler); | |
} | |
} | |
this.subscriptions.push(this.bindingEngine.propertyObserver(obj, property).subscribe(( newValue: any, oldValue: any ) => { | |
handler(propertyName, newValue, oldValue); | |
})); | |
} | |
} | |
} | |
public dispose(): void { | |
for ( let subscription of this.subscriptions ) { | |
if ( subscription && subscription.dispose ) { | |
subscription.dispose(); | |
} | |
} | |
} | |
} | |
export class UnitOfWorkObserver { | |
public unitOfWorks: {[index: string]: UnitOfWorkItem} = {}; | |
private subscriptions: Disposable[] = []; | |
private deepObserver: DeepObserver; | |
constructor( private bindingEngine: BindingEngine, | |
object: any, | |
properties: string[], | |
private paused: boolean ) { | |
this.deepObserver = new DeepObserver(bindingEngine); | |
this.observeForUnits(object, properties, this.unitOfWorks); | |
} | |
public toggleTracking( value: boolean ) { | |
this.paused = !value; | |
} | |
public hasChanges(): boolean { | |
return Object.keys(this.getChanges()).length > 0; | |
} | |
public getChanges(): {[index: string]: UnitOfWorkItem} { | |
let result: {[index: string]: UnitOfWorkItem} = {}; | |
if ( this.unitOfWorks ) { | |
for ( let key in this.unitOfWorks ) { | |
const value = this.unitOfWorks[key]; | |
if ( value && | |
( (Array.isArray(value.value) && (<UnitOfWorkItem[]>value.value).length > 0) || | |
(!Array.isArray(value.value) && | |
((typeof (value.value ) !== "object" && value.value !== value.oldValue) || | |
(typeof (value.value ) === "object" && CloneUtility.clone(value.value) !== value.oldValue)) )) ) { | |
result[key] = value; | |
} | |
} | |
} | |
return result; | |
} | |
public clearChanges() { | |
for ( const key in this.unitOfWorks ) { | |
delete this.unitOfWorks[key]; | |
} | |
this.unitOfWorks = {}; | |
} | |
public dispose() { | |
this.deepObserver.dispose(); | |
for ( let subscription of this.subscriptions ) { | |
if ( subscription && subscription.dispose ) { | |
subscription.dispose(); | |
} | |
} | |
} | |
private observeForUnits( object: any, properties: string[], unitOfWork: {[index: string]: UnitOfWorkItem} ) { | |
if ( !properties || properties.length === 0 ) { | |
properties = Reflection.getPropertiesOfObject(object); | |
} | |
const self = this; | |
for ( let i = 0; i < properties.length; i++ ) { | |
const propertyName: string = properties[i]; | |
// Determine the property name | |
const property = object ? propertyName : propertyName[1]; | |
// Determine the base object | |
const obj = object || propertyName[0]; | |
// Observe the array value or the property | |
const orig = CloneUtility.clone(obj[property]); | |
if ( Array.isArray(obj[property]) ) { | |
let unitOfWorkForProperty = unitOfWork[propertyName]; | |
//the value of an array unitofwork is another array of unit of works | |
//in it we store all recorded changes done to it. | |
if ( !unitOfWorkForProperty ) { | |
unitOfWorkForProperty = unitOfWork[propertyName] = { | |
action: UnitOfWorkAction.Modified, | |
oldValue: orig.slice(), //create a non referenced copy of the original array | |
value: [] | |
}; | |
} | |
for ( let arrayItem in obj[property] ) { | |
self.observeArrayItem(obj[property][arrayItem], unitOfWorkForProperty.value); | |
} | |
this.subscriptions.push(this.bindingEngine.collectionObserver(obj[property]).subscribe(( changedValues: any ) => { | |
if ( !self.paused ) { | |
self.handleArrayChange(unitOfWorkForProperty, obj[property], orig, changedValues); | |
} | |
})); | |
} else { | |
let oldValueProperty = CloneUtility.clone(obj[property]); | |
this.deepObserver.observe(obj, [propertyName], true, true, ( newValue: any, oldValue: any ) => { | |
if ( !self.paused ) { | |
self.handleChange(propertyName, unitOfWork, obj[property], oldValueProperty); | |
oldValueProperty = CloneUtility.clone(property); | |
} | |
}); | |
} | |
} | |
} | |
private handleArrayChange( unitOfWork: UnitOfWorkItem, | |
object: any[], | |
originalObject: any[], changedValues: any ) { | |
this.getUnitOfWorksForArrayChange(unitOfWork.value, originalObject, object, changedValues[0]); | |
} | |
private observeArrayItem( arrayItem: any, unitOfWorks: UnitOfWorkItem[] ) { | |
//observe array item properties in order to notice if it has been modified or not | |
const originalArrayItem = CloneUtility.clone(arrayItem); | |
const self = this; | |
this.deepObserver.observe(arrayItem, null, false, true, ( propertyName: string, | |
arrayItemPropertyNewValue: any, | |
arrayItemPropertyOldValue: any ) => { | |
if ( self.paused ) { | |
return; | |
} | |
let addNew = true; | |
//figure out whether we need to add a new unit of work item | |
let clonedArrayItem = CloneUtility.clone(arrayItem); | |
// let clonedArrayItemBeforeChange = CloneUtility.clone(arrayItem); | |
// clonedArrayItemBeforeChange[propertyName] = arrayItemPropertyOldValue; | |
for ( let i = unitOfWorks.length - 1; i >= 0; i-- ) { | |
const unitOfWork: UnitOfWorkItem = unitOfWorks[i]; | |
const clonedUnitOfWork = CloneUtility.clone(unitOfWork); | |
//if unit of work is Modified and oldValue = arrayItem | |
// -- splice it | |
if ( unitOfWork.action === UnitOfWorkAction.Modified && | |
clonedUnitOfWork.oldValue === clonedArrayItem ) { | |
unitOfWorks.splice(i, 1); | |
addNew = false; | |
break; | |
} | |
if ( unitOfWork.action === UnitOfWorkAction.Added && unitOfWork.value === arrayItem ) { | |
addNew = false; | |
break; | |
} | |
//UPDATE: We do need to as the object assigned to the unit of work is the observed one and referenced automatically managed! | |
//if unit of work is Added and value = arrayItem ( clone, adjust changed property with oldValue ) | |
// -- update unit of work value | |
// if ( unitOfWork.action === UnitOfWorkAction.Added && | |
// clonedUnitOfWork.value === clonedArrayItemBeforeChange ) { | |
// unitOfWork.value = arrayItem; | |
// addNew = false; | |
// break; | |
// } | |
} | |
if ( addNew ) { | |
unitOfWorks.push({ | |
action: UnitOfWorkAction.Modified, | |
value: arrayItem, | |
oldValue: originalArrayItem | |
}); | |
} | |
}); | |
} | |
private getUnitOfWorksForArrayChange( unitOfWorks: UnitOfWorkItem[], | |
originalObject: any[], | |
object: any[], | |
changedValues: any ) { | |
if ( !unitOfWorks ) { | |
unitOfWorks = []; | |
} | |
if ( changedValues.addedCount > 0 ) { | |
const addedItems: any[] = object.slice(changedValues.index, changedValues.index + changedValues.addedCount); | |
for ( let addedItem of addedItems ) { | |
let addNew = true; | |
for ( let i = unitOfWorks.length - 1; i >= 0; i-- ) { | |
const unitOfWork: UnitOfWorkItem = unitOfWorks[i]; | |
if ( unitOfWork.action === UnitOfWorkAction.Removed && unitOfWork.value === addedItem ) { | |
//remove item | |
addNew = false; | |
unitOfWorks.splice(i, 1); | |
} | |
} | |
if ( addNew ) { | |
unitOfWorks.push({ | |
action: UnitOfWorkAction.Added, | |
value: addedItem, | |
oldValue: null | |
}); | |
this.observeArrayItem(addedItem, unitOfWorks); | |
} | |
} | |
} | |
if ( changedValues.removed && changedValues.removed.length > 0 ) { | |
for ( let removedItem of changedValues.removed ) { | |
let addNew = true; | |
for ( let i = unitOfWorks.length - 1; i >= 0; i-- ) { | |
const unitOfWork: UnitOfWorkItem = unitOfWorks[i]; | |
if ( unitOfWork.action === UnitOfWorkAction.Added && unitOfWork.value === removedItem ) { | |
//remove item | |
addNew = false; | |
unitOfWorks.splice(i, 1); | |
} | |
} | |
if ( addNew ) { | |
unitOfWorks.push({ | |
action: UnitOfWorkAction.Removed, | |
value: removedItem, | |
oldValue: null | |
}); | |
} | |
} | |
} | |
} | |
private handleChange( propertyName: string, unitOfWork: {[index: string]: UnitOfWorkItem}, newValue: any, oldValue: any ) { | |
if ( this.paused ) { | |
return; | |
} | |
const existing = unitOfWork[propertyName]; | |
if ( existing ) { | |
existing.value = newValue; | |
} else { | |
//non array items can only be modified | |
unitOfWork[propertyName] = { | |
action: UnitOfWorkAction.Modified, | |
oldValue: oldValue, | |
value: newValue | |
}; | |
} | |
} | |
} | |
export interface UnitOfWorkItem { | |
action: UnitOfWorkAction; | |
value: any; | |
oldValue: any; | |
} | |
export enum UnitOfWorkAction { | |
Added, | |
Removed, | |
Modified | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment