Source: objectPool.js

/**
 * Enum for behavior of ObjectPool when the number of currently spawned objects exceeds the pool size
 * @readonly
 * @enum {Number}
 */
const POOL_BEHAVIOR_ON_OVERFLOW = { 
  /** The value for if the pool should just return null and not spawn any new objects */
  RETURN_NULL:0,
  /** The value for if the pool should spawn an object but not pool it */
  RETURN_NEW_AND_NOT_POOL:1,
  /** The value for if a pool should just expand its size to fit the new object being spawned */
  EXPAND_POOL:2 
}
/**
 * Enum for how objects are taken from the pool
 * @readonly
 * @enum {Number}
 */
const POOL_SELECTION = {
  /** Select the first unallocated pool object */
  FIRST:0,
  /** Select a random unallocated pool object */
  RANDOM:1
}

/**
 * Returns a new GameObject
 * @callback generatorCallback
 * @returns GameObject
 */

/**
 * Resets the properties of a GameObject to what they would be if it was a newly created object
 * @callback resetFuncCallback
 * @param {GameObject} obj object to reset
 */
 
/**
 * Destructor for a GameObject
 * @callback destroyFuncCallback
 * @param {GameObject} obj object to destroy
 */
 
/**
 * @typedef {Object} PooledInstance
 * @property {GameObject} object instance of object
 * @property {Boolean} isBeingUsed whether or not the pooled instance is currently allocated
 * @property {Array} cRef pointer to list of spawned objects in ObjectPool
 * @property {ObjectPool} object.pooledInstance pointer to ObjectPool that contains object
 */

/**
 * An object pool that handles pooling automatically. Useful for particle systems
 * @class ObjectPool
 * @extends SceneGraph
 * @param {String} name name of ObjectPool
 * @param {generatorCallback} generator 
 * @param {resetFuncCallback} resetFunc 
 * @param {?destroyFuncCallback} destroyFunc 
 * @param {Number} maxStore the maximum amount of objects that can be stored in the pool
 * @param {POOL_BEHAVIOR_ON_OVERFLOW} behaviorOnOverflow how the pool should react when too many objects are spawned
 * @param {POOL_SELECTION} [poolSelection=POOL_SELECTION.FIRST] how unallocated objects should be selected from the pool
 * @param {Boolean} [doUpdate=true] whether or not to update the objects in the pool
 * @param {Boolean} [doDraw=true] whether or not to draw the items in the pool
 * @param {Boolean} [clickable=false] Whether or not the items in the pool are clickable
 * @property {generatorCallback} objectConstructor the generator for new objects
 * @property {resetFuncCallback} resetFunc the function to apply to all spawned objects
 * @property {?destroyFuncCallback} destroyFunc the function to apply to all objects being destroyed, in case any cleanup is necessary
 * @property {Number} maxStore the maximum pool size
 * @property {Array} pooled the list of pooled objects, both allocated and unallocated
 * @property {Array} children the allocated objects in the pool (i.e. only spawned ones)
 * @property {POOL_SELECTION} select the selection method for allocation
 * @property {POOL_BEHAVIOR_ON_OVERFLOW} bof the pool behavior on overflow
 * @property {Boolean} _isAggregate=true true to indicate the object represents an aggregate of other objects
 */
var ObjectPool = function(name, generator, resetFunc, destroyFunc, maxStore, behaviorOnOverflow, poolSelection=POOL_SELECTION.FIRST, doUpdate=true, doDraw=true, clickable=false) {
  var self = this;
  
  /**
   * constructor
   * @function ObjectPool#constructor
   * @param {String} name name of ObjectPool
   * @param {generatorCallback} generator 
   * @param {resetFuncCallback} resetFunc 
   * @param {?destroyFuncCallback} destroyFunc 
   * @param {Number} maxStore the maximum amount of objects that can be stored in the pool
   * @param {POOL_BEHAVIOR_ON_OVERFLOW} behaviorOnOverflow how the pool should react when too many objects are spawned
   * @param {POOL_SELECTION} [poolSelection=POOL_SELECTION.FIRST] how unallocated objects should be selected from the pool
   * @param {Boolean} [doUpdate=true] whether or not to update the objects in the pool
   * @param {Boolean} [doDraw=true] whether or not to draw the items in the pool
   * @param {Boolean} [clickable=false] Whether or not the items in the pool are clickable
   */
  self.constructor = function(name, generator, resetFunc, destroyFunc, maxStore, behaviorOnOverflow, poolSelection, doUpdate=true, doDraw=true, clickable=false) {
    SceneGraph.call(self, name, doUpdate, doDraw, clickable);
    self.objectConstructor = generator;
    self.resetFunc = resetFunc;
    self.destroyFunc = destroyFunc;
    self.maxStore = maxStore;
    self.pooled = [];
    self.select = poolSelection;
    self.bof = behaviorOnOverflow;
    self._isAggregate = true;
  }

  /**
   * Returns new instance of pooled object
   * @function ObjectPool#newInstance
   * @return {PooledInstance} new instance of allocated object
   */
  self.newInstance = function() {
    var tmp = { object:self.objectConstructor(), isBeingUsed:true, cRef:self.children };
    tmp.object.pooledInstance = tmp;
    self.resetFunc(tmp.object);
    return tmp;
  }
  
  /**
   * Spawns a new object and adds it to the pool
   * @function ObjectPool#spawn
   * @returns {?PooledInstance} null if overflow and rules set to not allow overflow, else spawned object
   */
  self.spawn = function() {
    if (self.pooled.length < maxStore) {
      var obj = self.newInstance();
      self.pooled.push(obj);
      self.children.push(obj.object);
      return obj;
    }
    var available;
    if (self.select = POOL_SELECTION.FIRST) {
      available = self.pooled.find(function(e) {return !e.isBeingUsed;});
    } else {
      var l = self.pooled.filter(function(e) {return !e.isBeingUsed;});
      if (l.length == 0) {
        available = false;
      } else {
        available = l[Math.floor(Math.random() * l.length)];
      }
    }
    if (!available) {
      if (POOL_BEHAVIOR_ON_OVERFLOW.RETURN_NULL) {
        return null;
      } else if (POOL_BEHAVIOR_ON_OVERFLOW.RETURN_NEW_AND_NOT_POOL) {
        var obj = self.newInstance();
        self.children.push(obj.object);
        return obj;
      }
    }
    self.resetFunc(available.object);
    available.isBeingUsed = true;
    self.children.push(available.object);
    return available;
  }
  
  /**
   * Recycles a pooled instance from the pool
   * @param {PooledInstance} pooledObject thing to recycle
   * @function ObjectPool#recycle
   */
  self.recycle = function(pooledObject) {
    self.remove(pooledObject.object);
    if (self.destroyFunc) {
      self.destroyFunc(pooledObject.object);
    }
    pooledObject.isBeingUsed = false;
  }
  
  /**
   * Spawns multiple objects
   * @function ObjectPool#spawnSeveral
   * @param {Number} n how many objects to spawn
   * @returns {PooledInstance[]} list of spawned objects
   */
  self.spawnSeveral = function(n) {
    var out = [];
    for (var i = 0; i < n; ++i) {
      out.push(self.spawn());
    }
    return out;
  }

  self.constructor(name, generator, resetFunc, destroyFunc, maxStore, behaviorOnOverflow, poolSelection, doUpdate, doDraw, clickable);
  
  /**
   * Recycles/removes an object
   * @function ObjectPool#remove
   * @param {GameObject} e object to remove
   * @returns {GameObject} removed object
   */
  self.remove = function(e) {
    var f = self.pooled.find(function(p) {p.object === e});
    if (f) {
      f.isBeingUsed = false;
    }
    if (self.destroyFunc) {
      self.destroyFunc(e);
    }
    return self.removeIndex(self.indexOf(e));
  }
  
  /**
   * Recycles all objects in pool
   * @function ObjectPool#removeAll
   */
  self.removeAll = function() {
    self.pooled.forEach(function(e) {e.isBeingUsed = false;});
    if (self.destroyFunc) {self.children.forEach(function(e) {self.destroyFunc(e)})};
    self.children.splice(0, self.children.length);
  }
}