packages/reference-context/src/reference-context.js
/**
* Copyright 2017 Moshe Simantov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import BidiMap from 'bidi-map';
const kContext = Symbol('context');
const kParent = Symbol('parent');
/**
* A valid reference is any truthy value that is not boolean.
*
* @typedef {number|string|object|function|symbol} Reference
*/
/**
* @example
* import ReferenceContext from 'reference-context';
*
* const context = new ReferenceContext();
*/
export default class ReferenceContext {
/**
* Check if the given reference is valid.
* All truthy values are valid reference expects `true`.
*
* @param {*} reference The given reference
* @return {boolean} True if it's {Reference}
*/
static isValidReference(reference) {
switch (typeof reference) {
case 'object':
return reference !== null;
case 'function':
case 'symbol':
return true;
case 'string':
case 'number':
return !!reference;
case 'boolean':
case 'undefined':
default:
return false;
}
}
/**
* Create a reference-context instance.
*
* @param {object|ReferenceContext|Iterable} [context] Initialize context with values
* @param {ReferenceContext|null} [parentContext] The parent context for this context
*/
constructor(context = {}, parentContext = null) {
if (parentContext != null) {
if (!(parentContext instanceof ReferenceContext)) {
throw new TypeError(
`Expect parent context be instance of ReferenceContext: ${
parentContext
}`
);
}
/**
* @type {ReferenceContext|null}
* @private
*/
this[kParent] = parentContext;
} else if (context instanceof ReferenceContext) {
this[kParent] = context[kParent];
} else {
this[kParent] = null;
}
/**
* @type {BidiMap}
* @private
*/
this[kContext] = new BidiMap();
this.copyFrom(context);
}
/**
* Get the parent context.
*
* @type {ReferenceContext|null}
*/
get parent() {
return this[kParent];
}
/**
* The the number of keys in this context including it's parents.
*
* @type {number}
*/
get size() {
return this[kContext].size + (this[kParent] ? this[kParent].size : 0);
}
/**
* The the number of keys in this context (excluding the parents).
*
* @type {number}
*/
get ownSize() {
return this[kContext].size;
}
/**
* The the number of values in this context including it's parents.
*
* @type {number}
*/
get count() {
return this[kContext].count + (this[kParent] ? this[kParent].count : 0);
}
/**
* The the number of values in this context (excluding the parents).
*
* @type {number}
*/
get ownCount() {
return this[kContext].count;
}
/**
* Get the instance constructor name.
*
* @override
* @return {string}
*/
toString() {
return this.constructor.name;
}
/**
* Copy from other context it's references and values
*
* @param {ReferenceContext|Map|Array|object} context The context to copy from
* @return {ReferenceContext}
*/
copyFrom(context) {
if (context instanceof ReferenceContext || context instanceof Map) {
context.forEach((value, key) => this.set(key, value));
} else if (Array.isArray(context)) {
context.forEach(([key, value]) => this.set(key, value));
} else {
Object.keys(context).forEach(key => this.set(key, context[key]));
}
return this;
}
/**
* Iterate over the context own values.
*
* @param {function} callback
* @param {*} [thisArg]
* @return {ReferenceContext}
*/
forEach(callback, thisArg) {
this[kContext].forEach(callback, thisArg);
return this;
}
/**
* Create a closure context for this context.
*
* @return {ReferenceContext} The closure context
*/
closure() {
return new this.constructor({}, this);
}
/**
* Check if this context or it's parents has the given reference.
*
* @param {Reference} reference The given reference
* @return {boolean} True if it's has
*/
has(reference) {
if (this[kContext].has(reference)) return true;
if (!this[kParent]) return false;
return this[kParent].has(reference);
}
/**
* Check if this context only (and not the parents) has the given reference.
*
* @param {Reference} reference The given reference
* @return {boolean} True if it's has
*/
hasOwnReference(reference) {
return this[kContext].has(reference);
}
/**
* Get the reference value on this context or it's parents.
*
* @param {Reference} reference The reference of the value
* @throws {ReferenceError} If the reference not exists.
* @return {*|undefined} The reference value or undefined
*/
get(reference) {
if (this[kContext].has(reference)) {
return this[kContext].get(reference);
}
if (this[kParent]) {
return this[kParent].get(reference);
}
throw new ReferenceError(`Reference not exists: ${reference}`);
}
/**
* Set a reference for the given value.
*
* @param {Reference} reference The value reference
* @param {*} value The referable value
* @return {ReferenceContext}
*/
set(reference, value) {
if (!this.constructor.isValidReference(reference)) {
throw new TypeError(`The given reference is not valid: ${reference}`);
}
const oldValue = this[kContext].get(reference);
this[kContext].set(reference, value);
if (!this[kContext].exists(oldValue)) {
this.release(oldValue);
}
return this;
}
/**
* Check if this context or one of it's parents has the given value.
*
* @param {*} value The given value
* @return {boolean} True if it's does.
*/
exists(value) {
if (this[kContext].exists(value)) return true;
if (!this[kParent]) return false;
return this[kParent].exists(value);
}
/**
* Check if this context only (excluding the parents) has the given value.
*
* @param {*} value The given value
* @return {boolean} True if it's does.
*/
own(value) {
return this[kContext].exists(value);
}
/**
* Get the reference of the given value in this context or one of it's parents.
* Return undefined if the value not exists.
*
* @param {*} value The given value
* @throws {ReferenceError} If the value not exists.
* @return {Reference|undefined} The value reference or undefined
*/
lookup(value) {
const reference = this[kContext].getKeyOf(value);
if (reference !== undefined) return reference;
if (this[kParent]) {
return this[kParent].lookup(value);
}
throw new ReferenceError(`Value not exists: ${reference}`);
}
/**
* Assign a new reference for the given value or return it's current reference.
*
* @param {*} value The given value
* @throws {TypeError} If generating references is not supported in this context
* @return {Reference} The generated reference
*/
assign(value) {
if (this.exists(value)) {
return this.lookup(value);
}
let reference;
do {
reference = this.generateReference();
} while (this.has(reference));
this.set(reference, value);
return reference;
}
/**
* Generate a reference for the `assign` method.
* The reference not required to be unique, the callee is responsible to verify that the
* reference is not in use and if required call this method again.
*
* @throws {TypeError} If generating references is not supported in this context
* @return {Reference} The generated reference
*/
generateReference() {
throw new TypeError(`Assign is not supported at ${this}`);
}
/**
* Remove the given reference from this context.
* If the reference exists only on the parent contexts, this method will do nothing.
*
* @param {Reference} reference The given reference
* @return {boolean} True if the reference has been removed
*/
delete(reference) {
if (!this[kContext].has(reference)) return false;
const value = this[kContext].get(reference);
const references = this[kContext].getKeysOf(value);
this[kContext].delete(reference);
if (references.length === 1) {
this.release(value);
}
return true;
}
/**
* Release a value from this context and remove any reference for this value.
* If the value exists on the parent context, this method will do nothing.
*
* @param {*} value The value to release
* @return {boolean} True if the value released from this context
*/
release(value) {
const references = this[kContext].getKeysOf(value);
if (!references.length) return false;
references.forEach(reference => this.delete(reference));
return true;
}
/**
* Clear and release all the values on this context.
*
* @return {void}
*/
clear() {
this[kContext].clear();
}
}
/**
* @type {ReferenceContext.isValidReference}
*/
export const isValidReference = ReferenceContext.isValidReference;