Skip to content

Instantly share code, notes, and snippets.

@Kukks
Last active February 14, 2020 09:03
Show Gist options
  • Save Kukks/283c4a2e5e18fc334328c83e114530a5 to your computer and use it in GitHub Desktop.
Save Kukks/283c4a2e5e18fc334328c83e114530a5 to your computer and use it in GitHub Desktop.
Aurelia Unit Of Work Observer
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