| Index: extension/dart_proxy.js
|
| diff --git a/extension/dart_proxy.js b/extension/dart_proxy.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0cbd115d0592aa22416cd89aa772be5413218380
|
| --- /dev/null
|
| +++ b/extension/dart_proxy.js
|
| @@ -0,0 +1,446 @@
|
| + // We have to add this manually due to Chrome extension restrictions injecting arbitrary JS.
|
| + // TODO(jacobr): modify the jsinterop library so this is not required.
|
| + (function() {
|
| + // Proxy support
|
| +
|
| + // Table for local objects and functions that are proxied.
|
| + // TODO(vsm): Merge into one.
|
| + function ProxiedReferenceTable(name) {
|
| + // Name for debugging.
|
| + this.name = name;
|
| +
|
| + // Table from IDs to JS objects.
|
| + this.map = {};
|
| +
|
| + // Generator for new IDs.
|
| + this._nextId = 0;
|
| +
|
| + // Counter for deleted proxies.
|
| + this._deletedCount = 0;
|
| +
|
| + // Flag for one-time initialization.
|
| + this._initialized = false;
|
| +
|
| + // Ports for managing communication to proxies.
|
| + this.port = new ReceivePortSync();
|
| + this.sendPort = this.port.toSendPort();
|
| +
|
| + // Set of IDs that are global.
|
| + // These will not be freed on an exitScope().
|
| + this.globalIds = {};
|
| +
|
| + // Stack of scoped handles.
|
| + this.handleStack = [];
|
| +
|
| + // Stack of active scopes where each value is represented by the size of
|
| + // the handleStack at the beginning of the scope. When an active scope
|
| + // is popped, the handleStack is restored to where it was when the
|
| + // scope was entered.
|
| + this.scopeIndices = [];
|
| + }
|
| +
|
| + // Number of valid IDs. This is the number of objects (global and local)
|
| + // kept alive by this table.
|
| + ProxiedReferenceTable.prototype.count = function () {
|
| + return Object.keys(this.map).length;
|
| + }
|
| +
|
| + // Number of total IDs ever allocated.
|
| + ProxiedReferenceTable.prototype.total = function () {
|
| + return this.count() + this._deletedCount;
|
| + }
|
| +
|
| + // Adds an object to the table and return an ID for serialization.
|
| + ProxiedReferenceTable.prototype.add = function (obj) {
|
| + if (this.scopeIndices.length == 0) {
|
| + throw "Cannot allocate a proxy outside of a scope.";
|
| + }
|
| + // TODO(vsm): Cache refs for each obj?
|
| + var ref = this.name + '-' + this._nextId++;
|
| + this.handleStack.push(ref);
|
| + this.map[ref] = obj;
|
| + return ref;
|
| + }
|
| +
|
| + ProxiedReferenceTable.prototype._initializeOnce = function () {
|
| + if (!this._initialized) {
|
| + this._initialize();
|
| + }
|
| + this._initialized = true;
|
| + }
|
| +
|
| + // Overridable initialization on first use hook.
|
| + ProxiedReferenceTable.prototype._initialize = function () {}
|
| +
|
| + // Enters a new scope for this table.
|
| + ProxiedReferenceTable.prototype.enterScope = function() {
|
| + this._initializeOnce();
|
| + this.scopeIndices.push(this.handleStack.length);
|
| + }
|
| +
|
| + // Invalidates all non-global IDs in the current scope and
|
| + // exit the current scope.
|
| + ProxiedReferenceTable.prototype.exitScope = function() {
|
| + var start = this.scopeIndices.pop();
|
| + for (var i = start; i < this.handleStack.length; ++i) {
|
| + var key = this.handleStack[i];
|
| + if (!this.globalIds.hasOwnProperty(key)) {
|
| + delete this.map[this.handleStack[i]];
|
| + this._deletedCount++;
|
| + }
|
| + }
|
| + this.handleStack = this.handleStack.splice(0, start);
|
| + }
|
| +
|
| + // Makes this ID globally scope. It must be explicitly invalidated.
|
| + ProxiedReferenceTable.prototype.globalize = function(id) {
|
| + this.globalIds[id] = true;
|
| + }
|
| +
|
| + // Invalidates this ID, potentially freeing its corresponding object.
|
| + ProxiedReferenceTable.prototype.invalidate = function(id) {
|
| + var old = this.get(id);
|
| + delete this.globalIds[id];
|
| + delete this.map[id];
|
| + this._deletedCount++;
|
| + return old;
|
| + }
|
| +
|
| + // Gets the object or function corresponding to this ID.
|
| + ProxiedReferenceTable.prototype.get = function (id) {
|
| + if (!this.map.hasOwnProperty(id)) {
|
| + throw 'Proxy ' + id + ' has been invalidated.'
|
| + }
|
| + return this.map[id];
|
| + }
|
| +
|
| + // Subtype for managing function proxies.
|
| + function ProxiedFunctionTable() {}
|
| +
|
| + ProxiedFunctionTable.prototype = new ProxiedReferenceTable('func-ref');
|
| +
|
| + ProxiedFunctionTable.prototype._initialize = function () {
|
| + // Configure this table's port to invoke the corresponding function given
|
| + // its ID.
|
| + // TODO(vsm): Should we enter / exit a scope?
|
| + var table = this;
|
| +
|
| + this.port.receive(function (message) {
|
| + var id = message[0];
|
| + var args = message[1].map(deserialize);
|
| + var f = table.get(id);
|
| + // TODO(vsm): Should we capture _this_ automatically?
|
| + return serialize(f.apply(null, args));
|
| + });
|
| + }
|
| +
|
| + // The singleton table for proxied local functions.
|
| + var proxiedFunctionTable = new ProxiedFunctionTable();
|
| +
|
| + // Subtype for proxied local objects.
|
| + function ProxiedObjectTable() {}
|
| +
|
| + ProxiedObjectTable.prototype = new ProxiedReferenceTable('js-ref');
|
| +
|
| + ProxiedObjectTable.prototype._initialize = function () {
|
| + // Configure this table's port to forward methods, getters, and setters
|
| + // from the remote proxy to the local object.
|
| + var table = this;
|
| +
|
| + this.port.receive(function (message) {
|
| + // TODO(vsm): Support a mechanism to register a handler here.
|
| + var receiver = table.get(message[0]);
|
| + var method = message[1];
|
| + var args = message[2].map(deserialize);
|
| + if (method.indexOf("get:") == 0) {
|
| + // Getter.
|
| + var field = method.substring(4);
|
| + if (field in receiver && args.length == 0) {
|
| + return [ 'return', serialize(receiver[field]) ];
|
| + }
|
| + } else if (method.indexOf("set:") == 0) {
|
| + // Setter.
|
| + var field = method.substring(4);
|
| + if (args.length == 1) {
|
| + return [ 'return', serialize(receiver[field] = args[0]) ];
|
| + }
|
| + } else if (method == '[]' && args.length == 1) {
|
| + // Index getter.
|
| + return [ 'return', serialize(receiver[args[0]]) ];
|
| + } else {
|
| + var f = receiver[method];
|
| + if (f) {
|
| + try {
|
| + var result = f.apply(receiver, args);
|
| + return [ 'return', serialize(result) ];
|
| + } catch (e) {
|
| + return [ 'exception', serialize(e) ];
|
| + }
|
| + }
|
| + }
|
| + return [ 'none' ];
|
| + });
|
| + }
|
| +
|
| + // Singleton for local proxied objects.
|
| + var proxiedObjectTable = new ProxiedObjectTable();
|
| +
|
| + // DOM element serialization code.
|
| + var _localNextElementId = 0;
|
| + var _DART_ID = 'data-dart_id';
|
| + var _DART_TEMPORARY_ATTACHED = 'data-dart_temporary_attached';
|
| +
|
| + function serializeElement(e) {
|
| + // TODO(vsm): Use an isolate-specific id.
|
| + var id;
|
| + if (e.hasAttribute(_DART_ID)) {
|
| + id = e.getAttribute(_DART_ID);
|
| + } else {
|
| + id = (_localNextElementId++).toString();
|
| + e.setAttribute(_DART_ID, id);
|
| + }
|
| + if (e !== document.documentElement) {
|
| + // Element must be attached to DOM to be retrieve in js part.
|
| + // Attach top unattached parent to avoid detaching parent of "e" when
|
| + // appending "e" directly to document. We keep count of elements
|
| + // temporarily attached to prevent detaching top unattached parent to
|
| + // early. This count is equals to the length of _DART_TEMPORARY_ATTACHED
|
| + // attribute. There could be other elements to serialize having the same
|
| + // top unattached parent.
|
| + var top = e;
|
| + while (true) {
|
| + if (top.hasAttribute(_DART_TEMPORARY_ATTACHED)) {
|
| + var oldValue = top.getAttribute(_DART_TEMPORARY_ATTACHED);
|
| + var newValue = oldValue + "a";
|
| + top.setAttribute(_DART_TEMPORARY_ATTACHED, newValue);
|
| + break;
|
| + }
|
| + if (top.parentNode == null) {
|
| + top.setAttribute(_DART_TEMPORARY_ATTACHED, "a");
|
| + document.documentElement.appendChild(top);
|
| + break;
|
| + }
|
| + if (top.parentNode === document.documentElement) {
|
| + // e was already attached to dom
|
| + break;
|
| + }
|
| + top = top.parentNode;
|
| + }
|
| + }
|
| + return id;
|
| + }
|
| +
|
| + function deserializeElement(id) {
|
| + // TODO(vsm): Clear the attribute.
|
| + var list = document.querySelectorAll('[' + _DART_ID + '="' + id + '"]');
|
| +
|
| + if (list.length > 1) throw 'Non unique ID: ' + id;
|
| + if (list.length == 0) {
|
| + throw 'Element must be attached to the document: ' + id;
|
| + }
|
| + var e = list[0];
|
| + if (e !== document.documentElement) {
|
| + // detach temporary attached element
|
| + var top = e;
|
| + while (true) {
|
| + if (top.hasAttribute(_DART_TEMPORARY_ATTACHED)) {
|
| + var oldValue = top.getAttribute(_DART_TEMPORARY_ATTACHED);
|
| + var newValue = oldValue.substring(1);
|
| + top.setAttribute(_DART_TEMPORARY_ATTACHED, newValue);
|
| + // detach top only if no more elements have to be unserialized
|
| + if (top.getAttribute(_DART_TEMPORARY_ATTACHED).length === 0) {
|
| + top.removeAttribute(_DART_TEMPORARY_ATTACHED);
|
| + document.documentElement.removeChild(top);
|
| + }
|
| + break;
|
| + }
|
| + if (top.parentNode === document.documentElement) {
|
| + // e was already attached to dom
|
| + break;
|
| + }
|
| + top = top.parentNode;
|
| + }
|
| + }
|
| + return e;
|
| + }
|
| +
|
| +
|
| + // Type for remote proxies to Dart objects.
|
| + function DartProxy(id, sendPort) {
|
| + this.id = id;
|
| + this.port = sendPort;
|
| + }
|
| +
|
| + // Serializes JS types to SendPortSync format:
|
| + // - primitives -> primitives
|
| + // - sendport -> sendport
|
| + // - DOM element -> [ 'domref', element-id ]
|
| + // - Function -> [ 'funcref', function-id, sendport ]
|
| + // - Object -> [ 'objref', object-id, sendport ]
|
| + function serialize(message) {
|
| + if (message == null) {
|
| + return null; // Convert undefined to null.
|
| + } else if (typeof(message) == 'string' ||
|
| + typeof(message) == 'number' ||
|
| + typeof(message) == 'boolean') {
|
| + // Primitives are passed directly through.
|
| + return message;
|
| + } else if (message instanceof SendPortSync) {
|
| + // Non-proxied objects are serialized.
|
| + return message;
|
| + } else if (message instanceof Element) {
|
| + return [ 'domref', serializeElement(message) ];
|
| + } else if (typeof(message) == 'function') {
|
| + if ('_dart_id' in message) {
|
| + // Remote function proxy.
|
| + var remoteId = message._dart_id;
|
| + var remoteSendPort = message._dart_port;
|
| + return [ 'funcref', remoteId, remoteSendPort ];
|
| + } else {
|
| + // Local function proxy.
|
| + return [ 'funcref',
|
| + proxiedFunctionTable.add(message),
|
| + proxiedFunctionTable.sendPort ];
|
| + }
|
| + } else if (message instanceof DartProxy) {
|
| + // Remote object proxy.
|
| + return [ 'objref', message.id, message.port ];
|
| + } else {
|
| + // Local object proxy.
|
| + return [ 'objref',
|
| + proxiedObjectTable.add(message),
|
| + proxiedObjectTable.sendPort ];
|
| + }
|
| + }
|
| +
|
| + function deserialize(message) {
|
| + if (message == null) {
|
| + return null; // Convert undefined to null.
|
| + } else if (typeof(message) == 'string' ||
|
| + typeof(message) == 'number' ||
|
| + typeof(message) == 'boolean') {
|
| + // Primitives are passed directly through.
|
| + return message;
|
| + } else if (message instanceof SendPortSync) {
|
| + // Serialized type.
|
| + return message;
|
| + }
|
| + var tag = message[0];
|
| + switch (tag) {
|
| + case 'funcref': return deserializeFunction(message);
|
| + case 'objref': return deserializeObject(message);
|
| + case 'domref': return deserializeElement(message[1]);
|
| + }
|
| + throw 'Unsupported serialized data: ' + message;
|
| + }
|
| +
|
| + // Create a local function that forwards to the remote function.
|
| + function deserializeFunction(message) {
|
| + var id = message[1];
|
| + var port = message[2];
|
| + // TODO(vsm): Add a more robust check for a local SendPortSync.
|
| + if ("receivePort" in port) {
|
| + // Local function.
|
| + return proxiedFunctionTable.get(id);
|
| + } else {
|
| + // Remote function. Forward to its port.
|
| + var f = function () {
|
| + enterScope();
|
| + try {
|
| + var args = Array.prototype.slice.apply(arguments).map(serialize);
|
| + var result = port.callSync([id, args]);
|
| + return deserialize(result);
|
| + } finally {
|
| + exitScope();
|
| + }
|
| + };
|
| + // Cache the remote id and port.
|
| + f._dart_id = id;
|
| + f._dart_port = port;
|
| + return f;
|
| + }
|
| + }
|
| +
|
| + // Creates a DartProxy to forwards to the remote object.
|
| + function deserializeObject(message) {
|
| + var id = message[1];
|
| + var port = message[2];
|
| + // TODO(vsm): Add a more robust check for a local SendPortSync.
|
| + if ("receivePort" in port) {
|
| + // Local object.
|
| + return proxiedObjectTable.get(id);
|
| + } else {
|
| + // Remote object.
|
| + return new DartProxy(id, port);
|
| + }
|
| + }
|
| +
|
| + // Remote handler to construct a new JavaScript object given its
|
| + // serialized constructor and arguments.
|
| + function construct(args) {
|
| + args = args.map(deserialize);
|
| + var constructor = args[0];
|
| + args = Array.prototype.slice.call(args, 1);
|
| +
|
| + // Dummy Type with correct constructor.
|
| + var Type = function(){};
|
| + Type.prototype = constructor.prototype;
|
| +
|
| + // Create a new instance
|
| + var instance = new Type();
|
| +
|
| + // Call the original constructor.
|
| + var ret = constructor.apply(instance, args);
|
| +
|
| + return serialize(Object(ret) === ret ? ret : instance);
|
| + }
|
| +
|
| + // Remote handler to evaluate a string in JavaScript and return a serialized
|
| + // result.
|
| + function evaluate(data) {
|
| + return serialize(eval(deserialize(data)));
|
| + }
|
| +
|
| + // Remote handler for debugging.
|
| + function debug() {
|
| + var live = proxiedObjectTable.count() + proxiedFunctionTable.count();
|
| + var total = proxiedObjectTable.total() + proxiedFunctionTable.total();
|
| + return 'JS objects Live : ' + live +
|
| + ' (out of ' + total + ' ever allocated).';
|
| + }
|
| +
|
| + function makeGlobalPort(name, f) {
|
| + var port = new ReceivePortSync();
|
| + port.receive(f);
|
| + window.registerPort(name, port.toSendPort());
|
| + }
|
| +
|
| + // Enters a new scope in the JavaScript context.
|
| + function enterScope() {
|
| + proxiedObjectTable.enterScope();
|
| + proxiedFunctionTable.enterScope();
|
| + }
|
| +
|
| + // Exits the current scope (and invalidate local IDs) in the JavaScript
|
| + // context.
|
| + function exitScope() {
|
| + proxiedFunctionTable.exitScope();
|
| + proxiedObjectTable.exitScope();
|
| + }
|
| +
|
| + makeGlobalPort('dart-js-evaluate', evaluate);
|
| + makeGlobalPort('dart-js-create', construct);
|
| + makeGlobalPort('dart-js-debug', debug);
|
| + makeGlobalPort('dart-js-enter-scope', enterScope);
|
| + makeGlobalPort('dart-js-exit-scope', exitScope);
|
| + makeGlobalPort('dart-js-globalize', function(data) {
|
| + if (data[0] == "objref") return proxiedObjectTable.globalize(data[1]);
|
| + // TODO(vsm): Do we ever need to globalize functions?
|
| + throw 'Illegal type: ' + data[0];
|
| + });
|
| + makeGlobalPort('dart-js-invalidate', function(data) {
|
| + if (data[0] == "objref") return proxiedObjectTable.invalidate(data[1]);
|
| + // TODO(vsm): Do we ever need to globalize functions?
|
| + throw 'Illegal type: ' + data[0];
|
| + });
|
| +})();
|
|
|