immutable-tuple
Version:
Immutable finite list objects with constant-time equality testing (===) and no memory leaks
258 lines (221 loc) • 8.21 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
// Although `Symbol` is widely supported these days, we can safely fall
// back to using a non-enumerable string property without violating any
// assumptions elsewhere in the implementation.
var useSymbol =
typeof Symbol === "function" &&
typeof Symbol.for === "function";
// Used to mark `tuple.prototype` so that all objects that inherit from
// any `tuple.prototype` object (there could be more than one) will test
// positive according to `tuple.isTuple`.
var brand = useSymbol
? Symbol.for("immutable-tuple")
: "@@__IMMUTABLE_TUPLE__@@";
// Used to save a reference to the globally shared `UniversalWeakMap` that
// stores all known `tuple` objects.
var globalKey = useSymbol
? Symbol.for("immutable-tuple-root")
: "@@__IMMUTABLE_TUPLE_ROOT__@@";
// Convenient helper for defining hidden immutable properties.
function def(obj, name, value, enumerable) {
Object.defineProperty(obj, name, {
value: value,
enumerable: !! enumerable,
writable: false,
configurable: false
});
return value;
}
var freeze = Object.freeze || function (obj) {
return obj;
};
function isObjRef(value) {
switch (typeof value) {
case "object":
if (value === null) {
return false;
}
case "function":
return true;
default:
return false;
}
}
// The `mustConvertThisToArray` value is true when the corresponding
// `Array` method does not attempt to modify `this`, which means we can
// pass a `tuple` object as `this` without first converting it to an
// `Array`.
function forEachArrayMethod(fn) {
function call(name, mustConvertThisToArray) {
var desc = Object.getOwnPropertyDescriptor(Array.prototype, name);
fn(name, desc, !! mustConvertThisToArray);
}
call("every");
call("filter");
call("find");
call("findIndex");
call("forEach");
call("includes");
call("indexOf");
call("join");
call("lastIndexOf");
call("map");
call("reduce");
call("reduceRight");
call("slice");
call("some");
call("toLocaleString");
call("toString");
// The `reverse` and `sort` methods are usually destructive, but for
// `tuple` objects they return a new `tuple` object that has been
// appropriately reversed/sorted.
call("reverse", true);
call("sort", true);
// Make `[...someTuple]` work.
call(useSymbol && Symbol.iterator || "@@iterator");
}
// A map data structure that holds object keys weakly, yet can also hold
// non-object keys, unlike the native `WeakMap`.
var UniversalWeakMap = function UniversalWeakMap() {
// Since a `WeakMap` cannot hold primitive values as keys, we need a
// backup `Map` instance to hold primitive keys. Both `this._weakMap`
// and `this._strongMap` are lazily initialized.
this._weakMap = null;
this._strongMap = null;
this.data = null;
};
// Since `get` and `set` are the only methods used, that's all I've
// implemented here.
UniversalWeakMap.prototype.get = function get (key) {
var map = this._getMap(key, false);
if (map) {
return map.get(key);
}
};
UniversalWeakMap.prototype.set = function set (key, value) {
this._getMap(key, true).set(key, value);
// An actual `Map` or `WeakMap` would return `this` here, but
// returning the `value` is more convenient for the `tuple`
// implementation.
return value;
};
UniversalWeakMap.prototype._getMap = function _getMap (key, canCreate) {
if (! canCreate) {
return isObjRef(key) ? this._weakMap : this._strongMap;
}
if (isObjRef(key)) {
return this._weakMap || (this._weakMap = new WeakMap);
}
return this._strongMap || (this._strongMap = new Map);
};
// See [`universal-weak-map.js`](universal-weak-map.html).
// See [`util.js`](util.html).
// If this package is installed multiple times, there could be mutiple
// implementations of the `tuple` function with distinct `tuple.prototype`
// objects, but the shared pool of `tuple` objects must be the same across
// all implementations. While it would be ideal to use the `global`
// object, there's no reliable way to get the global object across all JS
// environments without using the `Function` constructor, so instead we
// use the global `Array` constructor as a shared namespace.
var root = Array[globalKey] || def(Array, globalKey, new UniversalWeakMap, false);
function lookup() {
return lookupArray(arguments);
}
function lookupArray(array) {
var node = root;
// Because we are building a tree of *weak* maps, the tree will not
// prevent objects in tuples from being garbage collected, since the
// tree itself will be pruned over time when the corresponding `tuple`
// objects become unreachable. In addition to internalization, this
// property is a key advantage of the `immutable-tuple` package.
var len = array.length;
for (var i = 0; i < len; ++i) {
var item = array[i];
node = node.get(item) || node.set(item, new UniversalWeakMap);
}
// Return node.data rather than node itself to prevent tampering with
// the UniversalWeakMap tree.
return node.data || (node.data = Object.create(null));
}
// See [`lookup.js`](lookup.html).
// See [`util.js`](util.html).
// When called with any number of arguments, this function returns an
// object that inherits from `tuple.prototype` and is guaranteed to be
// `===` any other `tuple` object that has exactly the same items. In
// computer science jargon, `tuple` instances are "internalized" or just
// "interned," which allows for constant-time equality checking, and makes
// it possible for tuple objects to be used as `Map` or `WeakMap` keys, or
// stored in a `Set`.
function tuple() {
var arguments$1 = arguments;
var node = lookup.apply(null, arguments);
if (node.tuple) {
return node.tuple;
}
var t = Object.create(tuple.prototype);
// Define immutable items with numeric indexes, and permanently fix the
// `.length` property.
var argc = arguments.length;
for (var i = 0; i < argc; ++i) {
t[i] = arguments$1[i];
}
def(t, "length", argc, false);
// Remember this new `tuple` object so that we can return the same object
// earlier next time.
return freeze(node.tuple = t);
}
// Since the `immutable-tuple` package could be installed multiple times
// in an application, there is no guarantee that the `tuple` constructor
// or `tuple.prototype` will be unique, so `value instanceof tuple` is
// unreliable. Instead, to test if a value is a tuple, you should use
// `tuple.isTuple(value)`.
def(tuple.prototype, brand, true, false);
function isTuple(that) {
return !! (that && that[brand] === true);
}
tuple.isTuple = isTuple;
function toArray(tuple) {
var array = [];
var i = tuple.length;
while (i--) { array[i] = tuple[i]; }
return array;
}
// Copy all generic non-destructive Array methods to `tuple.prototype`.
// This works because (for example) `Array.prototype.slice` can be invoked
// against any `Array`-like object.
forEachArrayMethod(function (name, desc, mustConvertThisToArray) {
var method = desc && desc.value;
if (typeof method === "function") {
desc.value = function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = method.apply(
mustConvertThisToArray ? toArray(this) : this,
args
);
// Of course, `tuple.prototype.slice` should return a `tuple` object,
// not a new `Array`.
return Array.isArray(result) ? tuple.apply(void 0, result) : result;
};
Object.defineProperty(tuple.prototype, name, desc);
}
});
// Like `Array.prototype.concat`, except for the extra effort required to
// convert any tuple arguments to arrays, so that
// ```
// tuple(1).concat(tuple(2), 3) === tuple(1, 2, 3)
// ```
var ref = Array.prototype;
var concat = ref.concat;
tuple.prototype.concat = function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return tuple.apply(void 0, concat.apply(toArray(this), args.map(
function (item) { return isTuple(item) ? toArray(item) : item; }
)));
};
exports.default = tuple;
exports.tuple = tuple;
exports.lookup = lookup;
exports.lookupArray = lookupArray;