Source: MultiKeyMap.js

// Convenient container abstraction for storing items referenced by a 3-tuple.
// Used by the ModifiersManager to store items by (property Name, entity, item ID).
// Methods starting with an underscore are private to the storage.
// This supports stackable items as it stores count for each 3-tuple.
// It is designed to be as fast as can be for a JS container.
/**
 * @class
 */
function MultiKeyMap()
{
	this.items = new Map();
	// Keys are referred to as 'primaryKey', 'secondaryKey', 'itemID'.
}

MultiKeyMap.prototype.Serialize = function()
{
	let ret = [];
	for (let primary of this.items.keys())
	{
		// Keys of a Map can be arbitrary types whereas objects only support string, so use a list.
		let vals = [primary, []];
		ret.push(vals);
		for (let secondary of this.items.get(primary).keys())
			vals[1].push([secondary, this.items.get(primary).get(secondary)]);
	}
	return ret;
};

MultiKeyMap.prototype.Deserialize = function(data)
{
	for (let primary in data)
	{
		this.items.set(data[primary][0], new Map());
		for (let secondary in data[primary][1])
			this.items.get(data[primary][0]).set(data[primary][1][secondary][0], data[primary][1][secondary][1]);
	}
};

/**
 * Add a single item.
 * NB: if you add an item with a different value but the same itemID, the original value remains.
 * @param item - an object.
 * @param itemID - internal ID of this item, for later removal and/or updating
 * @param stackable - if stackable, changing the count of items invalides, otherwise not.
 * @returns true if the items list changed in such a way that cached values are possibly invalidated.
 */
MultiKeyMap.prototype.AddItem = function(primaryKey, itemID, item, secondaryKey, stackable = false)
{
	if (!this._AddItem(primaryKey, itemID, item, secondaryKey, stackable))
		return false;

	this._OnItemModified(primaryKey, secondaryKey, itemID);
	return true;
};

/**
 * Add items to multiple properties at once (only one item per property)
 * @param items - Dictionnary of { primaryKey: item }
 * @returns true if the items list changed in such a way that cached values are possibly invalidated.
 */
MultiKeyMap.prototype.AddItems = function(itemID, items, secondaryKey, stackable = false)
{
	let modified = false;
	for (let primaryKey in items)
		modified = this.AddItem(primaryKey, itemID, items[primaryKey], secondaryKey, stackable) || modified;
	return modified;
};

/**
 * Removes a item on a property.
 * @param primaryKey - property to change (e.g. "Health/Max")
 * @param itemID - internal ID of the item to remove
 * @param secondaryKey - secondaryKey ID
 * @returns true if the items list changed in such a way that cached values are possibly invalidated.
 */
MultiKeyMap.prototype.RemoveItem = function(primaryKey, itemID, secondaryKey, stackable = false)
{
	if (!this._RemoveItem(primaryKey, itemID, secondaryKey, stackable))
		return false;

	this._OnItemModified(primaryKey, secondaryKey, itemID);
	return true;
};

/**
 * Removes items with this ID for any property name.
 * Naively iterates all property names.
 * @returns true if the items list changed in such a way that cached values are possibly invalidated.
 */
MultiKeyMap.prototype.RemoveAllItems = function(itemID, secondaryKey, stackable = false)
{
	let modified = false;
	// Map doesn't implement some so use a for-loop here.
	for (let primaryKey of this.items.keys())
		modified = this.RemoveItem(primaryKey, itemID, secondaryKey, stackable) || modified;
	return modified;
};

/**
 * @param itemID - internal ID of the item to try and find.
 * @returns true if there is at least one item with that itemID
 */
MultiKeyMap.prototype.HasItem = function(primaryKey, itemID, secondaryKey)
{
	// some() returns false for an empty list which is wanted here.
	return this._getItems(primaryKey, secondaryKey).some(item => item._ID === itemID);
};

/**
 * Check if we have a item for any property name.
 * Naively iterates all property names.
 * @returns true if there is at least one item with that itemID
 */
MultiKeyMap.prototype.HasAnyItem = function(itemID, secondaryKey)
{
	// Map doesn't implement some so use for loops instead.
	for (let primaryKey of this.items.keys())
		if (this.HasItem(primaryKey, itemID, secondaryKey))
			return true;
	return false;
};

/**
 * @returns A list of items (references to stored items to avoid copying)
 * (these need to be treated as constants to not break the map)
 */
MultiKeyMap.prototype.GetItems = function(primaryKey, secondaryKey)
{
	return this._getItems(primaryKey, secondaryKey);
};

/**
 * @returns A dictionary of { Property Name: items } for the secondary Key.
 * Naively iterates all property names.
 */
MultiKeyMap.prototype.GetAllItems = function(secondaryKey)
{
	let items = {};

	// Map doesn't implement filter so use a for loop.
	for (let primaryKey of this.items.keys())
	{
		if (!this.items.get(primaryKey).has(secondaryKey))
			continue;
		items[primaryKey] = this.GetItems(primaryKey, secondaryKey);
	}
	return items;
};

/**
 * @returns a list of items.
 * This does not necessarily return a reference to items' list, use _getItemsOrInit for that.
 */
MultiKeyMap.prototype._getItems = function(primaryKey, secondaryKey)
{
	let cache = this.items.get(primaryKey);
	if (cache)
		cache = cache.get(secondaryKey);
	return cache ? cache : [];
};

/**
 * @returns a reference to the list of items for that property name and secondaryKey.
 */
MultiKeyMap.prototype._getItemsOrInit = function(primaryKey, secondaryKey)
{
	let cache = this.items.get(primaryKey);
	if (!cache)
		cache = this.items.set(primaryKey, new Map()).get(primaryKey);

	let cache2 = cache.get(secondaryKey);
	if (!cache2)
		cache2 = cache.set(secondaryKey, []).get(secondaryKey);
	return cache2;
};

/**
 * @returns true if the items list changed in such a way that cached values are possibly invalidated.
 */
MultiKeyMap.prototype._AddItem = function(primaryKey, itemID, item, secondaryKey, stackable)
{
	let items = this._getItemsOrInit(primaryKey, secondaryKey);
	for (let it of items)
		if (it._ID == itemID)
		{
			it._count++;
			return stackable;
		}
	items.push({ "_ID": itemID, "_count": 1, "value": item });
	return true;
};

/**
 * @returns true if the items list changed in such a way that cached values are possibly invalidated.
 */
MultiKeyMap.prototype._RemoveItem = function(primaryKey, itemID, secondaryKey, stackable)
{
	let items = this._getItems(primaryKey, secondaryKey);

	let existingItem = items.filter(item => { return item._ID == itemID; });
	if (!existingItem.length)
		return false;

	if (--existingItem[0]._count > 0)
		return stackable;

	let stilValidItems = items.filter(item => item._count > 0);

	// Delete entries from the map if necessary to clean up.
	if (!stilValidItems.length)
	{
		this.items.get(primaryKey).delete(secondaryKey);
		if (!this.items.get(primaryKey).size)
			this.items.delete(primaryKey);
		return true;
	}

	this.items.get(primaryKey).set(secondaryKey, stilValidItems);

	return true;
};

/**
 * Stub method, to overload.
 */
MultiKeyMap.prototype._OnItemModified = function(primaryKey, secondaryKey, itemID) {};