Source: Destroyable.js

// 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" )
) );