Sunday, September 21, 2014

Tracking object references in JavaScript

It is a common case in AngularJS to have some model loaded on the main view (like list of objects) and to use these objects in other controllers (like the object details view). Usually it's done by holding the reference of the list object in other controller scope, to interact with this reference. Until these both objects point to each other (both references point to the same object) it's very fine. The changes from the details controller are reflected in a list and contrarywise.

But I frequently come across the situation where at least one of these objects is refreshed from the server (eg. in async comet event) and this relationship is lost. A lot of case-by-case code is required to be written to support such instances on the client side.

Today I've been thinking about tracking the object references generally in JavaScript and how it can be done. Unfortunately it looks that it can't. JavaScript doesn't really support real references like eg. in C language, that could be used to achieve this. In JS function there's no way to have access to reference that carries the object as the function argument, and thus to modify this reference.

But indeed there's a way to have access to the reference - using the closure. Closure holds references to all objects belonging to the closure scope. After a little time of playing I figured out some solution for such hypothetical reference tracker:

/**
* Creates new reference manager
* @constructor
*/
function Reference() {
function RefHolder() {
this.assignFunc = [];
}
/**
* Creates or updated the reference
* @param {object} value Value to be assigned
* @param {function(value)|object} funcOrObject Assignment function, assigning value to some variable
* accessible from closure or object when we are changing the assignment from one reference to another
*/
this.assign = function(value, funcOrObject) {
if (typeof funcOrObject == 'function') {
// this is the first assignment
// assign value to variable using func
funcOrObject(value);
// create value $ref to hold all references
if (!value.$ref)
value.$ref = new RefHolder();
value.$ref.assignFunc.push(funcOrObject);
} else {
// this is the reassignment
if (funcOrObject.$ref) {
for (var i=0; i<funcOrObject.$ref.assignFunc.length; i++) {
funcOrObject.$ref.assignFunc[i](value);
}
value.$ref = funcOrObject.$ref;
}
}
};
}
/********************************************************************
* Test code
********************************************************************/
function createJson(value) {
return {
id: 1,
value: 'root_'+value,
items: [
{
id: 2,
value: 'item2_'+value
},
{
id: 3,
value: 'item3_'+value
},
{
id: 4,
value: 'item4_'+value
}
]
}
}
var ref = new Reference();
// create two variables referencing to each other
var root1, root2;
ref.assign(createJson('A'), function(value) {root1 = value}); // assigns JSON A to root1
ref.assign(root1, function(value) {root2 = value}); // assigns root1 to root2
console.log('root1 after assignment', root1); // root_A
console.log('root2 after assignment', root2); // root_A
// modify single variable to see reflected changes for both
ref.assign(createJson('B'), root1); // changes the root1 assignment to JSON B - reflected in root2 assignment
console.log('root1 after reassignment', root1); // root_B
console.log('root2 after reassignment', root2); // root_B
And it works :)

Now, we have some more complex case. We are just re-assigning single object holding whole list, and we may have only the item (from the example above) assigned elsewhere. This is not as simple as the previous example, but feasible if we think about the model as persistent objects model. All persistent objects have some unique ID assigned. For example if you use UUID on server side, it can be UUID. If you use the numeric ID, it can be combination of ID and object type (class) etc. You can always figure out easily some other method to have unique ID for you objects some way.

In such instance, to achieve the goal we need to have the previous JSON structure, and to scan new one, looking for objects with the same ID, and then reuse our "reference tracker" above. Here is full source including the new usage:

/**
* Creates new reference manager
* @param {function(object)} idFunc returning string representing unique ID of object
* @constructor
*/
function Reference(idFunc) {
this.idFunc = idFunc;
function RefHolder(id) {
this.id = id;
this.assignFunc = [];
}
/**
* Creates or updated the reference
* @param {object} value Value to be assigned
* @param {function(value)|object} funcOrObject Assignment function, assigning value to some variable
* accessible from closure or object when we are changing the assignment from one reference to another
*/
this.assign = function(value, funcOrObject) {
if (typeof funcOrObject == 'function') {
// this is the first assignment
// assign value to variable using func
funcOrObject(value);
// create value $ref to hold all references
if (!value.$ref)
value.$ref = new RefHolder(this.idFunc ? this.idFunc(value) : null);
value.$ref.assignFunc.push(funcOrObject);
} else {
// this is the reassignment
if (funcOrObject.$ref) {
for (var i=0; i<funcOrObject.$ref.assignFunc.length; i++) {
funcOrObject.$ref.assignFunc[i](value);
}
value.$ref = funcOrObject.$ref;
}
}
};
/**
* Scans object recursively to look for all objects
* @param {object} object Object to scan
* @param {object[]} array All objects contained 'object'
*/
function scanR(object, array) {
array.push(object);
for (var prop in object)
if (prop!='$ref' && typeof object[prop] == 'object') {
if (object[prop] instanceof Array) {
for (var i=0; i<object[prop].length; i++)
scanR(object[prop][i], array);
} else
scanR(object[prop], array);
}
}
/**
* Reassigns object recursively. The object should be previously assigned and a proper idFunc should
* be given for this Reference.
* @param {object} value New reference
* @param {object} object Previous reference
*/
this.assignR = function(value, object) {
if (!idFunc)
throw new Exception('This Reference has no idFunc defined');
var oldObjects = [];
var newObjects = [];
scanR(object, oldObjects);
scanR(value, newObjects);
// lookup associated refs
var oldRefs = {};
var newRefs = {};
for (var i=0; i<oldObjects.length; i++)
if (oldObjects[i].$ref)
oldRefs[oldObjects[i].$ref.id] = oldObjects[i];
for (i=0; i<newObjects.length; i++)
if (idFunc(newObjects[i]))
newRefs[idFunc(newObjects[i])] = newObjects[i];
// reassign
for (var id in oldRefs)
if (newRefs[id])
this.assign(newRefs[id], oldRefs[id]);
};
/**
* Cleans up the reference variable from $ref for JSON serialization.
* @param object
*/
this.cleanup = function(object) {
var clean = {};
for (var prop in object) {
if (prop!='$ref')
clean[prop] = object[prop];
}
return clean;
}
}
/********************************************************************
* Test code
********************************************************************/
function createJson(value) {
return {
id: 1,
value: 'root_'+value,
items: [
{
id: 2,
value: 'item2_'+value
},
{
id: 3,
value: 'item3_'+value
},
{
id: 4,
value: 'item4_'+value
}
]
}
}
var ref = new Reference(function(object) {
return object.id;
});
// create two variables referencing to each other
var root1, root2;
ref.assign(createJson('A'), function(value) {root1 = value}); // assigns JSON to root1
ref.assign(root1, function(value) {root2 = value}); // assigns root1 to root2
console.log('root1 after assignment', root1); // root_A
console.log('root2 after assignment', root2); // root_A
// modify single variable to see reflected changes for both
ref.assign(createJson('B'), root1); // changes the root1 assignment - reflected in root2 assignment
console.log('root1 after reassignment', root1); // root_B
console.log('root2 after reassignment', root2); // root_B
// now we will test something complex - recursive assignment
var item;
ref.assign(createJson('C'), function(value) {root1 = value}); // assigns JSON to root1
ref.assign(root1.items[0], function(value) {item = value}); // assigns items[0] to item
console.log('root1 after assignment', root1); // root_C
console.log('item after assignment', item); // item_C
ref.assignR(createJson('D'), root1); // assigns new JSON to root1, results in root1 and item
// assignment due to proper idFunc
console.log('root1 after reassignmentR', root1); // root_D
console.log('item after reassignmentR', item); // item_D
To have it clean, I  also added cleanup() function to clean the objects from $ref reference, to have clean objects for JSON representation to be sent to server.