OLD | NEW |
(Empty) | |
| 1 // We have to add this manually due to Chrome extension restrictions injecting
arbitrary JS. |
| 2 // TODO(jacobr): modify the jsinterop library so this is not required. |
| 3 (function() { |
| 4 // Proxy support |
| 5 |
| 6 // Table for local objects and functions that are proxied. |
| 7 // TODO(vsm): Merge into one. |
| 8 function ProxiedReferenceTable(name) { |
| 9 // Name for debugging. |
| 10 this.name = name; |
| 11 |
| 12 // Table from IDs to JS objects. |
| 13 this.map = {}; |
| 14 |
| 15 // Generator for new IDs. |
| 16 this._nextId = 0; |
| 17 |
| 18 // Counter for deleted proxies. |
| 19 this._deletedCount = 0; |
| 20 |
| 21 // Flag for one-time initialization. |
| 22 this._initialized = false; |
| 23 |
| 24 // Ports for managing communication to proxies. |
| 25 this.port = new ReceivePortSync(); |
| 26 this.sendPort = this.port.toSendPort(); |
| 27 |
| 28 // Set of IDs that are global. |
| 29 // These will not be freed on an exitScope(). |
| 30 this.globalIds = {}; |
| 31 |
| 32 // Stack of scoped handles. |
| 33 this.handleStack = []; |
| 34 |
| 35 // Stack of active scopes where each value is represented by the size of |
| 36 // the handleStack at the beginning of the scope. When an active scope |
| 37 // is popped, the handleStack is restored to where it was when the |
| 38 // scope was entered. |
| 39 this.scopeIndices = []; |
| 40 } |
| 41 |
| 42 // Number of valid IDs. This is the number of objects (global and local) |
| 43 // kept alive by this table. |
| 44 ProxiedReferenceTable.prototype.count = function () { |
| 45 return Object.keys(this.map).length; |
| 46 } |
| 47 |
| 48 // Number of total IDs ever allocated. |
| 49 ProxiedReferenceTable.prototype.total = function () { |
| 50 return this.count() + this._deletedCount; |
| 51 } |
| 52 |
| 53 // Adds an object to the table and return an ID for serialization. |
| 54 ProxiedReferenceTable.prototype.add = function (obj) { |
| 55 if (this.scopeIndices.length == 0) { |
| 56 throw "Cannot allocate a proxy outside of a scope."; |
| 57 } |
| 58 // TODO(vsm): Cache refs for each obj? |
| 59 var ref = this.name + '-' + this._nextId++; |
| 60 this.handleStack.push(ref); |
| 61 this.map[ref] = obj; |
| 62 return ref; |
| 63 } |
| 64 |
| 65 ProxiedReferenceTable.prototype._initializeOnce = function () { |
| 66 if (!this._initialized) { |
| 67 this._initialize(); |
| 68 } |
| 69 this._initialized = true; |
| 70 } |
| 71 |
| 72 // Overridable initialization on first use hook. |
| 73 ProxiedReferenceTable.prototype._initialize = function () {} |
| 74 |
| 75 // Enters a new scope for this table. |
| 76 ProxiedReferenceTable.prototype.enterScope = function() { |
| 77 this._initializeOnce(); |
| 78 this.scopeIndices.push(this.handleStack.length); |
| 79 } |
| 80 |
| 81 // Invalidates all non-global IDs in the current scope and |
| 82 // exit the current scope. |
| 83 ProxiedReferenceTable.prototype.exitScope = function() { |
| 84 var start = this.scopeIndices.pop(); |
| 85 for (var i = start; i < this.handleStack.length; ++i) { |
| 86 var key = this.handleStack[i]; |
| 87 if (!this.globalIds.hasOwnProperty(key)) { |
| 88 delete this.map[this.handleStack[i]]; |
| 89 this._deletedCount++; |
| 90 } |
| 91 } |
| 92 this.handleStack = this.handleStack.splice(0, start); |
| 93 } |
| 94 |
| 95 // Makes this ID globally scope. It must be explicitly invalidated. |
| 96 ProxiedReferenceTable.prototype.globalize = function(id) { |
| 97 this.globalIds[id] = true; |
| 98 } |
| 99 |
| 100 // Invalidates this ID, potentially freeing its corresponding object. |
| 101 ProxiedReferenceTable.prototype.invalidate = function(id) { |
| 102 var old = this.get(id); |
| 103 delete this.globalIds[id]; |
| 104 delete this.map[id]; |
| 105 this._deletedCount++; |
| 106 return old; |
| 107 } |
| 108 |
| 109 // Gets the object or function corresponding to this ID. |
| 110 ProxiedReferenceTable.prototype.get = function (id) { |
| 111 if (!this.map.hasOwnProperty(id)) { |
| 112 throw 'Proxy ' + id + ' has been invalidated.' |
| 113 } |
| 114 return this.map[id]; |
| 115 } |
| 116 |
| 117 // Subtype for managing function proxies. |
| 118 function ProxiedFunctionTable() {} |
| 119 |
| 120 ProxiedFunctionTable.prototype = new ProxiedReferenceTable('func-ref'); |
| 121 |
| 122 ProxiedFunctionTable.prototype._initialize = function () { |
| 123 // Configure this table's port to invoke the corresponding function given |
| 124 // its ID. |
| 125 // TODO(vsm): Should we enter / exit a scope? |
| 126 var table = this; |
| 127 |
| 128 this.port.receive(function (message) { |
| 129 var id = message[0]; |
| 130 var args = message[1].map(deserialize); |
| 131 var f = table.get(id); |
| 132 // TODO(vsm): Should we capture _this_ automatically? |
| 133 return serialize(f.apply(null, args)); |
| 134 }); |
| 135 } |
| 136 |
| 137 // The singleton table for proxied local functions. |
| 138 var proxiedFunctionTable = new ProxiedFunctionTable(); |
| 139 |
| 140 // Subtype for proxied local objects. |
| 141 function ProxiedObjectTable() {} |
| 142 |
| 143 ProxiedObjectTable.prototype = new ProxiedReferenceTable('js-ref'); |
| 144 |
| 145 ProxiedObjectTable.prototype._initialize = function () { |
| 146 // Configure this table's port to forward methods, getters, and setters |
| 147 // from the remote proxy to the local object. |
| 148 var table = this; |
| 149 |
| 150 this.port.receive(function (message) { |
| 151 // TODO(vsm): Support a mechanism to register a handler here. |
| 152 var receiver = table.get(message[0]); |
| 153 var method = message[1]; |
| 154 var args = message[2].map(deserialize); |
| 155 if (method.indexOf("get:") == 0) { |
| 156 // Getter. |
| 157 var field = method.substring(4); |
| 158 if (field in receiver && args.length == 0) { |
| 159 return [ 'return', serialize(receiver[field]) ]; |
| 160 } |
| 161 } else if (method.indexOf("set:") == 0) { |
| 162 // Setter. |
| 163 var field = method.substring(4); |
| 164 if (args.length == 1) { |
| 165 return [ 'return', serialize(receiver[field] = args[0]) ]; |
| 166 } |
| 167 } else if (method == '[]' && args.length == 1) { |
| 168 // Index getter. |
| 169 return [ 'return', serialize(receiver[args[0]]) ]; |
| 170 } else { |
| 171 var f = receiver[method]; |
| 172 if (f) { |
| 173 try { |
| 174 var result = f.apply(receiver, args); |
| 175 return [ 'return', serialize(result) ]; |
| 176 } catch (e) { |
| 177 return [ 'exception', serialize(e) ]; |
| 178 } |
| 179 } |
| 180 } |
| 181 return [ 'none' ]; |
| 182 }); |
| 183 } |
| 184 |
| 185 // Singleton for local proxied objects. |
| 186 var proxiedObjectTable = new ProxiedObjectTable(); |
| 187 |
| 188 // DOM element serialization code. |
| 189 var _localNextElementId = 0; |
| 190 var _DART_ID = 'data-dart_id'; |
| 191 var _DART_TEMPORARY_ATTACHED = 'data-dart_temporary_attached'; |
| 192 |
| 193 function serializeElement(e) { |
| 194 // TODO(vsm): Use an isolate-specific id. |
| 195 var id; |
| 196 if (e.hasAttribute(_DART_ID)) { |
| 197 id = e.getAttribute(_DART_ID); |
| 198 } else { |
| 199 id = (_localNextElementId++).toString(); |
| 200 e.setAttribute(_DART_ID, id); |
| 201 } |
| 202 if (e !== document.documentElement) { |
| 203 // Element must be attached to DOM to be retrieve in js part. |
| 204 // Attach top unattached parent to avoid detaching parent of "e" when |
| 205 // appending "e" directly to document. We keep count of elements |
| 206 // temporarily attached to prevent detaching top unattached parent to |
| 207 // early. This count is equals to the length of _DART_TEMPORARY_ATTACHED |
| 208 // attribute. There could be other elements to serialize having the same |
| 209 // top unattached parent. |
| 210 var top = e; |
| 211 while (true) { |
| 212 if (top.hasAttribute(_DART_TEMPORARY_ATTACHED)) { |
| 213 var oldValue = top.getAttribute(_DART_TEMPORARY_ATTACHED); |
| 214 var newValue = oldValue + "a"; |
| 215 top.setAttribute(_DART_TEMPORARY_ATTACHED, newValue); |
| 216 break; |
| 217 } |
| 218 if (top.parentNode == null) { |
| 219 top.setAttribute(_DART_TEMPORARY_ATTACHED, "a"); |
| 220 document.documentElement.appendChild(top); |
| 221 break; |
| 222 } |
| 223 if (top.parentNode === document.documentElement) { |
| 224 // e was already attached to dom |
| 225 break; |
| 226 } |
| 227 top = top.parentNode; |
| 228 } |
| 229 } |
| 230 return id; |
| 231 } |
| 232 |
| 233 function deserializeElement(id) { |
| 234 // TODO(vsm): Clear the attribute. |
| 235 var list = document.querySelectorAll('[' + _DART_ID + '="' + id + '"]'); |
| 236 |
| 237 if (list.length > 1) throw 'Non unique ID: ' + id; |
| 238 if (list.length == 0) { |
| 239 throw 'Element must be attached to the document: ' + id; |
| 240 } |
| 241 var e = list[0]; |
| 242 if (e !== document.documentElement) { |
| 243 // detach temporary attached element |
| 244 var top = e; |
| 245 while (true) { |
| 246 if (top.hasAttribute(_DART_TEMPORARY_ATTACHED)) { |
| 247 var oldValue = top.getAttribute(_DART_TEMPORARY_ATTACHED); |
| 248 var newValue = oldValue.substring(1); |
| 249 top.setAttribute(_DART_TEMPORARY_ATTACHED, newValue); |
| 250 // detach top only if no more elements have to be unserialized |
| 251 if (top.getAttribute(_DART_TEMPORARY_ATTACHED).length === 0) { |
| 252 top.removeAttribute(_DART_TEMPORARY_ATTACHED); |
| 253 document.documentElement.removeChild(top); |
| 254 } |
| 255 break; |
| 256 } |
| 257 if (top.parentNode === document.documentElement) { |
| 258 // e was already attached to dom |
| 259 break; |
| 260 } |
| 261 top = top.parentNode; |
| 262 } |
| 263 } |
| 264 return e; |
| 265 } |
| 266 |
| 267 |
| 268 // Type for remote proxies to Dart objects. |
| 269 function DartProxy(id, sendPort) { |
| 270 this.id = id; |
| 271 this.port = sendPort; |
| 272 } |
| 273 |
| 274 // Serializes JS types to SendPortSync format: |
| 275 // - primitives -> primitives |
| 276 // - sendport -> sendport |
| 277 // - DOM element -> [ 'domref', element-id ] |
| 278 // - Function -> [ 'funcref', function-id, sendport ] |
| 279 // - Object -> [ 'objref', object-id, sendport ] |
| 280 function serialize(message) { |
| 281 if (message == null) { |
| 282 return null; // Convert undefined to null. |
| 283 } else if (typeof(message) == 'string' || |
| 284 typeof(message) == 'number' || |
| 285 typeof(message) == 'boolean') { |
| 286 // Primitives are passed directly through. |
| 287 return message; |
| 288 } else if (message instanceof SendPortSync) { |
| 289 // Non-proxied objects are serialized. |
| 290 return message; |
| 291 } else if (message instanceof Element) { |
| 292 return [ 'domref', serializeElement(message) ]; |
| 293 } else if (typeof(message) == 'function') { |
| 294 if ('_dart_id' in message) { |
| 295 // Remote function proxy. |
| 296 var remoteId = message._dart_id; |
| 297 var remoteSendPort = message._dart_port; |
| 298 return [ 'funcref', remoteId, remoteSendPort ]; |
| 299 } else { |
| 300 // Local function proxy. |
| 301 return [ 'funcref', |
| 302 proxiedFunctionTable.add(message), |
| 303 proxiedFunctionTable.sendPort ]; |
| 304 } |
| 305 } else if (message instanceof DartProxy) { |
| 306 // Remote object proxy. |
| 307 return [ 'objref', message.id, message.port ]; |
| 308 } else { |
| 309 // Local object proxy. |
| 310 return [ 'objref', |
| 311 proxiedObjectTable.add(message), |
| 312 proxiedObjectTable.sendPort ]; |
| 313 } |
| 314 } |
| 315 |
| 316 function deserialize(message) { |
| 317 if (message == null) { |
| 318 return null; // Convert undefined to null. |
| 319 } else if (typeof(message) == 'string' || |
| 320 typeof(message) == 'number' || |
| 321 typeof(message) == 'boolean') { |
| 322 // Primitives are passed directly through. |
| 323 return message; |
| 324 } else if (message instanceof SendPortSync) { |
| 325 // Serialized type. |
| 326 return message; |
| 327 } |
| 328 var tag = message[0]; |
| 329 switch (tag) { |
| 330 case 'funcref': return deserializeFunction(message); |
| 331 case 'objref': return deserializeObject(message); |
| 332 case 'domref': return deserializeElement(message[1]); |
| 333 } |
| 334 throw 'Unsupported serialized data: ' + message; |
| 335 } |
| 336 |
| 337 // Create a local function that forwards to the remote function. |
| 338 function deserializeFunction(message) { |
| 339 var id = message[1]; |
| 340 var port = message[2]; |
| 341 // TODO(vsm): Add a more robust check for a local SendPortSync. |
| 342 if ("receivePort" in port) { |
| 343 // Local function. |
| 344 return proxiedFunctionTable.get(id); |
| 345 } else { |
| 346 // Remote function. Forward to its port. |
| 347 var f = function () { |
| 348 enterScope(); |
| 349 try { |
| 350 var args = Array.prototype.slice.apply(arguments).map(serialize); |
| 351 var result = port.callSync([id, args]); |
| 352 return deserialize(result); |
| 353 } finally { |
| 354 exitScope(); |
| 355 } |
| 356 }; |
| 357 // Cache the remote id and port. |
| 358 f._dart_id = id; |
| 359 f._dart_port = port; |
| 360 return f; |
| 361 } |
| 362 } |
| 363 |
| 364 // Creates a DartProxy to forwards to the remote object. |
| 365 function deserializeObject(message) { |
| 366 var id = message[1]; |
| 367 var port = message[2]; |
| 368 // TODO(vsm): Add a more robust check for a local SendPortSync. |
| 369 if ("receivePort" in port) { |
| 370 // Local object. |
| 371 return proxiedObjectTable.get(id); |
| 372 } else { |
| 373 // Remote object. |
| 374 return new DartProxy(id, port); |
| 375 } |
| 376 } |
| 377 |
| 378 // Remote handler to construct a new JavaScript object given its |
| 379 // serialized constructor and arguments. |
| 380 function construct(args) { |
| 381 args = args.map(deserialize); |
| 382 var constructor = args[0]; |
| 383 args = Array.prototype.slice.call(args, 1); |
| 384 |
| 385 // Dummy Type with correct constructor. |
| 386 var Type = function(){}; |
| 387 Type.prototype = constructor.prototype; |
| 388 |
| 389 // Create a new instance |
| 390 var instance = new Type(); |
| 391 |
| 392 // Call the original constructor. |
| 393 var ret = constructor.apply(instance, args); |
| 394 |
| 395 return serialize(Object(ret) === ret ? ret : instance); |
| 396 } |
| 397 |
| 398 // Remote handler to evaluate a string in JavaScript and return a serialized |
| 399 // result. |
| 400 function evaluate(data) { |
| 401 return serialize(eval(deserialize(data))); |
| 402 } |
| 403 |
| 404 // Remote handler for debugging. |
| 405 function debug() { |
| 406 var live = proxiedObjectTable.count() + proxiedFunctionTable.count(); |
| 407 var total = proxiedObjectTable.total() + proxiedFunctionTable.total(); |
| 408 return 'JS objects Live : ' + live + |
| 409 ' (out of ' + total + ' ever allocated).'; |
| 410 } |
| 411 |
| 412 function makeGlobalPort(name, f) { |
| 413 var port = new ReceivePortSync(); |
| 414 port.receive(f); |
| 415 window.registerPort(name, port.toSendPort()); |
| 416 } |
| 417 |
| 418 // Enters a new scope in the JavaScript context. |
| 419 function enterScope() { |
| 420 proxiedObjectTable.enterScope(); |
| 421 proxiedFunctionTable.enterScope(); |
| 422 } |
| 423 |
| 424 // Exits the current scope (and invalidate local IDs) in the JavaScript |
| 425 // context. |
| 426 function exitScope() { |
| 427 proxiedFunctionTable.exitScope(); |
| 428 proxiedObjectTable.exitScope(); |
| 429 } |
| 430 |
| 431 makeGlobalPort('dart-js-evaluate', evaluate); |
| 432 makeGlobalPort('dart-js-create', construct); |
| 433 makeGlobalPort('dart-js-debug', debug); |
| 434 makeGlobalPort('dart-js-enter-scope', enterScope); |
| 435 makeGlobalPort('dart-js-exit-scope', exitScope); |
| 436 makeGlobalPort('dart-js-globalize', function(data) { |
| 437 if (data[0] == "objref") return proxiedObjectTable.globalize(data[1]); |
| 438 // TODO(vsm): Do we ever need to globalize functions? |
| 439 throw 'Illegal type: ' + data[0]; |
| 440 }); |
| 441 makeGlobalPort('dart-js-invalidate', function(data) { |
| 442 if (data[0] == "objref") return proxiedObjectTable.invalidate(data[1]); |
| 443 // TODO(vsm): Do we ever need to globalize functions? |
| 444 throw 'Illegal type: ' + data[0]; |
| 445 }); |
| 446 })(); |
OLD | NEW |