// Licensed Materials - Property of IBM
//
// IBM Watson Analytics
//
// (C) Copyright IBM Corp. 2015, 2018
//
// US Government Users Restricted Rights - Use, duplication or
// disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
module.exports = ( function(
ObjectPolyfill,
decl,
WeakMap,
WeakSet
)
{
"use strict";
/*jshint latedef:false*/
var DestroyableKey = null;
// Try to use an ES6 symbol and be very protective of the destroyed flag.
var DestroyedObjects = new WeakSet();
// grab Array.prototype.map
var map = Array.prototype.map;
/**
* Takes a _target and attempts to resolve it to a Destroyable, even if it is an interface proxy.
* @memberof module:barejs.Destroyable~
* @private
*/
function resolveTarget( _target )
{
return decl.isProxy( _target ) && _target.is( Destroyable ) ? _target.as( DestroyableKey ) : Object( _target );
}
/**
* Check if _target is destroyed.
* @param {module:barejs.Destroyable~Destroyable} _target The destroyable to check
* @returns {boolean} True if the target has been destroyed, false otherwise.
* @memberof module:barejs.Destroyable
*/
function isDestroyed( _target )
{
return !!_target && DestroyedObjects.has( resolveTarget( _target ) );
}
/**
* @class module:barejs.Destroyable~MetaData
* @private
*/
function MetaData()
{
this.listeners = [];
this.references = [];
this.referenceHandlers = [];
}
decl.declareClass( MetaData,
/** @lends module:barejs.Destroyable~MetaData */
{
listeners: null,
references: null,
referenceHandlers: null
} );
/**
* Anonymous function closure to keep the WeakMap protected. By using a WeakMap MetaData objects
* have no explicit link back to the Destroyable objects.
* @param {WeakMap} _map WeakMap to store the MetaData objects in.
* @ignore
*/
( function( _map )
{
/**
* Get MetaData for a Destroyable
* @param {module:barejs.Destroyable} _destroyable The Destroyable instance
* @param {boolean} _create Set to true to create the metadata if it doesn't exist.
* @returns {module:barejs.Destroyable~MetaData} The meta data, or null (if create is false and there is none)
*/
MetaData.get = function( _destroyable, _create )
{
var meta = _map.get( _destroyable ) || null;
if ( ( !meta ) && ( _create === true ) )
_map.set( _destroyable, meta = new MetaData() );
return meta;
};
/**
* Remove MetaData for a Destroyable.
* @param {module:barejs.Destroyable} _destroyable The Destroyable instance
* @returns {module:barejs.Destroyable~MetaData} The removed meta data, or null
*/
MetaData.remove = function( _destroyable )
{
var meta = _map.get( _destroyable ) || null;
if ( meta )
_map["delete"]( _destroyable );
return meta;
};
}( new WeakMap() ) );
/**
* function that can be applied to an array to remove an item
* @this {Array}
* @memberof module:barejs.Destroyable~
* @private
*/
function Array_remove( _item )
{
/*jshint validthis:true*/
var idx = this.indexOf( _item );
/*istanbul ignore else: the if statement is purely a sanity check*/
if ( idx >= 0 )
this.splice( idx, 1 );
return idx >= 0;
}
/**
* Helper function that will destroy _target, if it is not null/undefined.
* @param {module:barejs.Destroyable} _target The target to destroy.
* @memberof module:barejs.Destroyable~
* @private
*/
function destroyTarget( _target/*, _index*/ )
{
if ( !_target )
return;
_target = Object( _target );
if ( "destroy" in _target )
_target.destroy();
// Compatibility with dojo handles
else if ( "remove" in _target )
_target.remove();
}
/**
* @class module:barejs.Destroyable~OwnedHandle
* @private
* @classdesc Tracks an ownership and destroys links when needed
*/
function OwnedHandle( _owner, _target )
{
this.owner = _owner;
this.target = _target;
_owner.addDestroyListener( this._owner_destroyed = this._owner_destroyed.bind( this ) );
if ( this.target instanceof Destroyable )
this.target.addDestroyListener( this._target_destroyed = this._target_destroyed.bind( this ) );
}
decl.declareClass( OwnedHandle,
/** @lends module:barejs.Destroyable~OwnedHandle# */
{
owner: null,
target: null,
_owner_destroyed: function()
{
/*istanbul ignore next: We actively remove listeners on destroy, so we don't expect to hit this safety guard*/
if ( !this.target )
return;
// Destroyable::ref will take care of deleting any reference to this object
// By deleting the owner before calling destroy, we are sure _target_destroyed will not
// perform any logic
delete this.owner;
// We are about to call destroy, so there's no point in getting the notification
if ( this.target instanceof Destroyable )
this.target.removeDestroyListener( this._target_destroyed );
destroyTarget( this.target );
delete this._owner_destroyed;
delete this._target_destroyed;
delete this.target;
},
_target_destroyed: function()
{
/*istanbul ignore next: We actively remove listeners on destroy, so we don't expect to hit this safety guard*/
if ( !this.owner )
return;
this.owner.removeDestroyListener( this._owner_destroyed );
delete this._owner_destroyed;
delete this._target_destroyed;
delete this.owner;
delete this.target;
}
} );
/**
* @class module:barejs.Destroyable
* @classdesc Class that adds lifetime management to JavaScript objects. Destroyable provides a few features to ensure
* the JavaScript garbage collector can recollect memory as soon as possible.
*
* A Destroyable instance can own other objects. The ownership in this case implies: if this instance is destroyed,
* the instance I own should also be destroyed. Destroyable can own any object having either a destroy or remove method.
*
* A Destroyable instance can also ref other objects. The reference is automatically cleared on destroy. If the ref-ed
* object is a Destroyable, the reference is also cleared if the target gets destroyed.
*
* Owned objects are also destroyed if this Destroyable is destroyed.
* Referenced objects are automatically unlinked if this object is destroyed, and even if the referenced
* is destroyed (in case the referenced object is Destroyable).
*/
function Destroyable()
{
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ )
Destroyable.alive.add( this );
}
DestroyableKey = decl.preventCast( Destroyable );
// Protected hidden functions of Destroyable
/**
* Code that actually ties a target to an owner
* @param {module:barejs.Destroyable} _target The target to own
* @this {module:barejs.Destroyable}
* @memberof module:barejs.Destroyable~
* @private
*/
function __own( _target/*used as iterator callback*/ )
{
// jshint validthis:true
var actualTarget = resolveTarget( _target );
if ( !actualTarget || !( ( "destroy" in actualTarget ) || ( "remove" in actualTarget ) ) )
throw new TypeError( "Cannot own; invalid value" );
else if ( actualTarget === this )
throw new Error( "An object cannot own itself" );
// Note the use of the comma operator; we don't need to store a reference to the OwnedHandle instance,
// it just needs to be created. Using the comma operator avoid jshint warnings about using new for side-effects.
return new OwnedHandle( this, actualTarget ), _target;
}
// Static array to track created Destroyable objects that haven't been destroyed yet
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ )
decl.defineProperty( Destroyable, "alive", { value: new WeakSet() } );
// All methods of Destroyable are non-enumerable and protected from modification
return decl.declareClass( Destroyable,
/** @lends module:barejs.Destroyable */
{
// Don't inherit static alive/isDestroyed values
$private:
{
value: function( _key )
{
return _key === "isDestroyed" || _key === "alive";
}
},
// Protect isDestroyed method from tampering. We're not exposing this on instance level since it's
// not a common operation.
isDestroyed: { value: isDestroyed }
},
/** @lends module:barejs.Destroyable# */
{
/**
* Destroy method that will notify registered listeners and clean up references.
* @function
*/
destroy:
{
// Subclasses must be able to add custom destroy logic, so allow writing destroy
writable: true,
value: function destroy()
{
if ( isDestroyed( this ) )
return;
var meta = MetaData.remove( this ), i, len;
if ( meta )
{
// Invoke listeners before clearing references, in case listeners need to look at them
if ( meta.listeners.length > 0 )
{
for ( i = 0, len = meta.listeners.length; i < len; ++i )
meta.listeners[i]( this );
// Also explicitly clear the array to drop references to listeners
meta.listeners.length = 0;
}
// Clear references
if ( meta.references.length > 0 )
{
// Don't call unref since it will mutate the references array
for ( i = 0, len = meta.references.length; i < len; ++i )
{
if ( meta.referenceHandlers[i] )
resolveTarget( this[ meta.references[i] ] ).removeDestroyListener( meta.referenceHandlers[i] );
delete this[ meta.references[i] ];
}
// Clear the arrays
meta.references.length = 0;
meta.referenceHandlers.length = 0;
}
}
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ )
Destroyable.alive["delete"]( this );
DestroyedObjects.add( this );
}
},
/**
* Register a destroy listener for this Destroyable object.
* @function
* @param {module:barejs.Destroyable~DestroyListener} _listener The listener function to add.
* @returns {function} the listener
*/
addDestroyListener:
{
value: function addDestroyListener( _listener )
{
var meta = MetaData.get( this, true );
meta.listeners.push( ObjectPolyfill.ensureCallable( _listener ) );
return _listener;
}
},
/**
* Unregister a destroy listener for this Destroyable object.
* @function
* @param {module:barejs.Destroyable~DestroyListener} _listener The listener function to remove.
*/
removeDestroyListener:
{
value: function removeDestroyListener( _listener )
{
var meta = MetaData.get( this, false );
return ( !!meta ) && Array_remove.call( meta.listeners, _listener );
}
},
/**
* Own a number of handles. Returns an array of the owned handles.
* @function
* @returns {Array} The owned handles.
*/
own:
{
value: function own( /*...*/ )
{
return map.call( arguments, __own, this );
}
},
/**
* Reference a target as a member property that will be unlinked on destroy.
* If the referenced target is also Destroyable, the ref is also cleared if the target is destroyed.
* @function
* @param {string} _name The name to reference.
* @param {object} _target The object to assign to the reference.
* @returns The value of this[_name].
*/
ref:
{
value: function ref( _name, _target )
{
if ( typeof _name !== "string" )
throw new TypeError( "Name must be a string" );
// typeof null === "object", but we don't want to allow it
switch ( _target === null ? "undefined" : typeof _target )
{
// Functions can create a circular reference via closures (or being bound to this).
case "function":
// Referencing objects might cause circular references
case "object":
// So we allow referencing them
break;
case "undefined":
throw new TypeError( "_target cannot be " + _target + ". Use unref to clear a reference." );
default:
throw new TypeError( "_target cannot be " + ( typeof _target ) + ". Only objects or functions can be referenced." );
}
var meta = MetaData.get( this, true ),
idx = meta.references.indexOf( _name ),
actualTarget;
if ( idx < 0 ) // Add reference
{
idx = meta.references.push( _name ) - 1;
meta.referenceHandlers.push( null );
}
else if ( meta.referenceHandlers[idx] ) // Update/change reference
{
actualTarget = resolveTarget( this[_name] );
if ( actualTarget instanceof Destroyable )
actualTarget.removeDestroyListener( meta.referenceHandlers[idx] );
meta.referenceHandlers[idx] = null;
}
decl.defineProperty( this, _name,
{
configurable: true,
enumerable: ObjectPolyfill.shouldBeEnumerable( _name ),
value: _target
} );
actualTarget = resolveTarget( _target );
// If the thing we are referencing is a Destroyable, ensure it is unref-ed if the target gets destroyed.
if ( actualTarget instanceof Destroyable )
actualTarget.addDestroyListener( meta.referenceHandlers[idx] = this.unref.bind( this, _name ) );
return _target;
}
},
/**
* Remove a reference (by name). If the name was given to ownMember, the member is NOT
* removed from the list of owned targets.
* Does NOT destroy the value currently referenced.
* @function
* @param {string} _name the name to remove the reference to
* @param {object|function} [_value] If a value is provided (and is not null), unref will only clear the reference if _value equals whatever is currently ref-ed.
*/
unref:
{
value: function unref( _name )
{
if ( typeof _name !== "string" )
throw new TypeError( "_name must be a string" );
var result/* = undefined*/, value = arguments[1], meta, idx, handler;
if ( ( meta = MetaData.get( this, false ) ) && ( ( idx = meta.references.indexOf( _name ) ) >= 0 ) )
{
result = this[ meta.references[idx] ];
// If a second argument is supplied, we validate it equals the value to unref
if ( value && !decl.is( result, value ) )
{
result = undefined; // value did not match
}
else
{
delete this[ meta.references.splice( idx, 1 )[0] ];
// Since we splice the name, we need to splice the handlers too; otherwise the references and handlers will go out of sync.
// If a "target" is provided, assume we got called as a destroy listener; in that case we don't unregister since we got here from that listener.
if ( ( handler = meta.referenceHandlers.splice( idx, 1 )[0] ) && ( !value ) )
resolveTarget( result ).removeDestroyListener( handler );
}
}
return result;
}
},
/**
* The ownMember function combines ref and own into 1 call. The target is owned and then ref-ed as _name.
* @function
* @param {string} _name The name of the member.
* @param {module:barejs.Destroyable} _target The target to own.
* @returns The owned _target
*/
ownMember:
{
value: function ownMember( _name, _target )
{
// use __own directly since it avoids the overhead of Array.prototype.map
// Also, call __own before ref, since it does some stricter validation.
return this.ref( _name, __own.call( this, _target ) );
}
},
/**
* Utility method that will iterate a collection and destroy all items in it.
* @function
* @param {object} _collection An object with a forEach method or length property (e.g. an Array).
*/
destroyAll:
{
value: function destroyAll( _collection )
{
var c = Object( _collection );
if ( "forEach" in c )
c.forEach( destroyTarget, null );
else if ( "length" in c )
Array.prototype.forEach.call( c, destroyTarget, null );
else
throw new TypeError( "_collection must either have a forEach method or a length property." );
}
}
} );
/**
* Destroy listeners are called with one argument; the Destroyed object.
* @callback module:barejs.Destroyable~DestroyListener
* @param {module:barejs.Destroyable} _destroyed The Destroyable that got destroyed.
*/
// End of module
}(
require( "./polyfill/Object" ),
require( "./decl" ),
require( "./WeakMap" ),
require( "./WeakSet" )
) );