UNPKG

proxyequal

Version:

A proxy based usage tracking and comparison

185 lines (154 loc) 6.3 kB
/* * Copyright 2016 Google Inc. All rights reserved. * * 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. */ export function proxyPolyfill() { var lastRevokeFn = null; var ProxyPolyfill; /** * @param {*} o * @return {boolean} whether this is probably a (non-null) Object */ function isObject(o) { return o ? typeof o === 'object' || typeof o === 'function' : false; } /** * @constructor * @param {!Object} target * @param {{apply, construct, get, set}} handler */ ProxyPolyfill = function ProxyPolyfill(target, handler) { if (!isObject(target) || !isObject(handler)) { throw new TypeError('Cannot create proxy with a non-object as target or handler'); } // Construct revoke function, and set lastRevokeFn so that Proxy.revocable can steal it. // The caller might get the wrong revoke function if a user replaces or wraps scope.Proxy // to call itself, but that seems unlikely especially when using the polyfill. var throwRevoked = function throwRevoked() {}; lastRevokeFn = function lastRevokeFn() { throwRevoked = function throwRevoked(trap) { throw new TypeError("Cannot perform '" + trap + "' on a proxy that has been revoked"); }; }; // Fail on unsupported traps: Chrome doesn't do this, but ensure that users of the polyfill // are a bit more careful. Copy the internal parts of handler to prevent user changes. var unsafeHandler = handler; handler = { 'get': null, 'set': null, 'apply': null, 'construct': null }; for (var k in unsafeHandler) { if (!(k in handler)) {//throw new TypeError(`Proxy polyfill does not support trap '${k}'`); } else { handler[k] = unsafeHandler[k]; } } if (typeof unsafeHandler === 'function') { // Allow handler to be a function (which has an 'apply' method). This matches what is // probably a bug in native versions. It treats the apply call as a trap to be configured. handler.apply = unsafeHandler.apply.bind(unsafeHandler); } // Define proxy as this, or a Function (if either it's callable, or apply is set). // TODO(samthor): Closure compiler doesn't know about 'construct', attempts to rename it. var proxy = this; var isMethod = false; var isArray = false; if (typeof target === 'function') { proxy = function ProxyPolyfill() { var usingNew = this && this.constructor === proxy; var args = Array.prototype.slice.call(arguments); throwRevoked(usingNew ? 'construct' : 'apply'); if (usingNew && handler['construct']) { return handler['construct'].call(this, target, args); } else if (!usingNew && handler.apply) { return handler.apply(target, this, args); } // since the target was a function, fallback to calling it directly. if (usingNew) { // inspired by answers to https://stackoverflow.com/q/1606797 args.unshift(target); // pass class as first arg to constructor, although irrelevant // nb. cast to convince Closure compiler that this is a constructor var f = /** @type {!Function} */ target.bind.apply(target, args); return new f(); } return target.apply(this, args); }; isMethod = true; } else if (target instanceof Array) { proxy = []; isArray = true; } // Create default getters/setters. Create different code paths as handler.get/handler.set can't // change after creation. var getter = handler.get ? function (prop) { throwRevoked('get'); return handler.get(this, prop, proxy); } : function (prop) { throwRevoked('get'); return this[prop]; }; var setter = handler.set ? function (prop, value) { throwRevoked('set'); handler.set(this, prop, value, proxy); } : function (prop, value) { throwRevoked('set'); this[prop] = value; }; // Clone direct properties (i.e., not part of a prototype). var propertyNames = Object.getOwnPropertyNames(target); var propertyMap = {}; propertyNames.forEach(function (prop) { if ((isMethod || isArray) && prop in proxy) { return; // ignore properties already here, e.g. 'bind', 'prototype' etc } var real = Object.getOwnPropertyDescriptor(target, prop); var desc = { enumerable: !!real.enumerable, get: getter.bind(target, prop), set: setter.bind(target, prop) }; Object.defineProperty(proxy, prop, desc); propertyMap[prop] = true; }); // Set the prototype, or clone all prototype methods (always required if a getter is provided). // TODO(samthor): We don't allow prototype methods to be set. It's (even more) awkward. // An alternative here would be to _just_ clone methods to keep behavior consistent. var prototypeOk = true; if (Object.setPrototypeOf) { Object.setPrototypeOf(proxy, Object.getPrototypeOf(target)); } else if (proxy.__proto__) { proxy.__proto__ = target.__proto__; } else { prototypeOk = false; } if (handler.get || !prototypeOk) { for (var _k in target) { if (propertyMap[_k]) { continue; } Object.defineProperty(proxy, _k, { get: getter.bind(target, _k) }); } } // The Proxy polyfill cannot handle adding new properties. Seal the target and proxy. Object.seal(target); Object.seal(proxy); return proxy; // nb. if isMethod is true, proxy != this }; ProxyPolyfill.revocable = function (target, handler) { var p = new ProxyPolyfill(target, handler); return { 'proxy': p, 'revoke': lastRevokeFn }; }; return ProxyPolyfill; }