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]; |
+ }); |
+})(); |