runtime-branding
Version:
Runtime Branding API for TypeScript
157 lines • 4.83 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const brand = Symbol();
const targetType = Symbol();
// TODO: do we need a global branding map?
/**
* Creates a new brand, identified by the provided branding object.
*
* @param brandObject An object that represents the brand. The shape of this object should be unique, and symbols may be used as unique keys.
* @param callback An optional callback with variable arguments which is invoked during object branding
*/
function createBranding(brandObject, callback) {
const brandedObjects = new WeakSet();
function has(obj) {
return brandedObjects.has(obj);
}
function assert(obj) {
if (!brandedObjects.has(obj)) {
throw new Error("Object not branded.");
}
}
function branding(obj) {
if (has(obj)) {
throw new Error("Object already branded.");
}
brandedObjects.add(obj);
if (callback) {
try {
callback(obj, brandObject);
}
catch (e) {
brandedObjects.delete(obj);
throw e;
}
}
return obj;
}
function refine(newBrand, newCallback) {
const b = Object.assign({}, newBrand, brandObject);
const refinedBranding = createBranding(b, newCallback);
return merge(refinedBranding);
}
function merge(branding1) {
return mixin(branding, branding1);
}
branding.has = has; // maybe all these should be built separately, then Object.assigned to branding()
branding.assert = assert;
branding.refine = refine;
branding.merge = merge;
branding[brand] = brandObject;
return branding;
}
exports.createBranding = createBranding;
/**
* Mixes two different brandings in a single one.
*
* @param fn1 The first branding function
* @param fn2 The second branding function
*/
function mixin(fn1, fn2) {
function has(obj) {
return fn1.has(obj) && fn2.has(obj);
}
function assert(obj) {
fn1.assert(obj);
fn2.assert(obj);
}
function branding(obj) {
fn1(obj);
fn2(obj);
return obj;
}
function refine(newBrand, callback) {
const b = Object.assign({}, newBrand, fn1[brand], fn2[brand]);
return createBranding(b, callback);
}
function merge(branding1) {
return mixin(branding, branding1);
}
const newBrand = Object.assign({}, fn1[brand], fn2[brand]);
branding.has = has;
branding.assert = assert;
branding.refine = refine;
branding.merge = merge;
branding[brand] = newBrand;
return branding;
}
const typeSym = Symbol();
function type(typeId, ...base) {
return buildTypeBranding(typeId, ...base);
}
exports.type = type;
function buildTypeBranding(typeId, ...base) {
const brandObj = typeBrand(typeId, ...base);
const branding = createBranding(brandObj, (o) => typeCallback(o, typeBranding));
const typeBranding = Object.assign(branding, brandObj);
const has = typeBranding.has;
typeBranding.has = function (obj) {
const proto = Object.getPrototypeOf(obj);
return has(obj) || (!!proto && has(proto));
};
const assert = typeBranding.assert;
typeBranding.assert = function (obj) {
if (!typeBranding.has(obj)) {
assert(obj);
}
};
if (typeId instanceof Function) {
typeBranding(typeId.prototype);
}
return typeBranding;
}
const objMap = new WeakMap();
function typeCallback(obj, brand) {
if (!objMap.has(obj)) { // only for leaf
objMap.set(obj, brand);
brand.ancestors.forEach(a => a(obj));
}
}
function typeOf(obj) {
if (obj instanceof Function) {
obj = obj.prototype;
}
const res = objMap.get(obj) || objMap.get(Object.getPrototypeOf(obj));
if (!res) {
throw new Error('Type ID not found for object');
}
return res;
}
exports.typeOf = typeOf;
function isTyped(obj) {
return objMap.has(obj);
}
exports.isTyped = isTyped;
function assertType(type, obj) {
const assert = type.assert;
assert(obj);
}
exports.assertType = assertType;
function isTypeBranding(fn) {
return fn[typeSym] === null;
}
exports.isTypeBranding = isTypeBranding;
function typeBrand(typeId, ...base) {
return {
[typeSym]: null,
typeId,
base: new Set(base),
ancestors: new Set(ancestorsOf(base))
};
}
function ancestorsOf(base) {
let ancestors = base;
base.forEach(t => (ancestors = ancestors.concat(Array.from(t.ancestors))));
return new Set(ancestors);
}
//# sourceMappingURL=generic_branding.js.map