 Chromium Code Reviews
 Chromium Code Reviews Issue 15782009:
  RFC: introduce dart:js  (Closed) 
  Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart
    
  
    Issue 15782009:
  RFC: introduce dart:js  (Closed) 
  Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart| OLD | NEW | 
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * The js.dart library provides simple JavaScript invocation from Dart that | |
| 7 * works on both Dartium and on other modern browsers via Dart2JS. | |
| 8 * | |
| 9 * It provides a model based on scoped [JsObject] objects. Proxies give Dart | |
| 10 * code access to JavaScript objects, fields, and functions as well as the | |
| 11 * ability to pass Dart objects and functions to JavaScript functions. Scopes | |
| 12 * enable developers to use proxies without memory leaks - a common challenge | |
| 13 * with cross-runtime interoperation. | |
| 14 * | |
| 15 * The top-level [context] getter provides a [JsObject] to the global JavaScript | |
| 16 * context for the page your Dart code is running on. In the following example: | |
| 17 * | |
| 18 * import 'dart:js'; | |
| 19 * | |
| 20 * void main() { | |
| 21 * context.callMethod('alert', ['Hello from Dart via JavaScript']); | |
| 22 * } | |
| 23 * | |
| 24 * context['alert'] creates a proxy to the top-level alert function in | |
| 25 * JavaScript. It is invoked from Dart as a regular function that forwards to | |
| 26 * the underlying JavaScript one. By default, proxies are released when | |
| 27 * the currently executing event completes, e.g., when main is completes | |
| 28 * in this example. | |
| 29 * | |
| 30 * The library also enables JavaScript proxies to Dart objects and functions. | |
| 31 * For example, the following Dart code: | |
| 32 * | |
| 33 * context['dartCallback'] = new Callback.once((x) => print(x*2)); | |
| 34 * | |
| 35 * defines a top-level JavaScript function 'dartCallback' that is a proxy to | |
| 36 * the corresponding Dart function. The [Callback.once] constructor allows the | |
| 37 * proxy to the Dart function to be retained across multiple events; | |
| 38 * instead it is released after the first invocation. (This is a common | |
| 39 * pattern for asychronous callbacks.) | |
| 40 * | |
| 41 * Note, parameters and return values are intuitively passed by value for | |
| 42 * primitives and by reference for non-primitives. In the latter case, the | |
| 43 * references are automatically wrapped and unwrapped as proxies by the library. | |
| 44 * | |
| 45 * This library also allows construction of JavaScripts objects given a | |
| 46 * [JsObject] to a corresponding JavaScript constructor. For example, if the | |
| 47 * following JavaScript is loaded on the page: | |
| 48 * | |
| 49 * function Foo(x) { | |
| 50 * this.x = x; | |
| 51 * } | |
| 52 * | |
| 53 * Foo.prototype.add = function(other) { | |
| 54 * return new Foo(this.x + other.x); | |
| 55 * } | |
| 56 * | |
| 57 * then, the following Dart: | |
| 58 * | |
| 59 * var foo = new JsObject(context['Foo'], [42]); | |
| 60 * var foo2 = foo.callMethod('add', [foo]); | |
| 61 * print(foo2['x']); | |
| 62 * | |
| 63 * will construct a JavaScript Foo object with the parameter 42, invoke its | |
| 64 * add method, and return a [JsObject] to a new Foo object whose x field is 84. | |
| 65 */ | |
| 66 | |
| 67 library dart.js; | |
| 68 | |
| 69 import 'dart:async'; | |
| 70 import 'dart:html'; | |
| 71 import 'dart:isolate'; | |
| 72 import 'dart:mirrors'; | |
| 73 | |
| 74 // JavaScript bootstrapping code. | |
| 75 // TODO(vsm): Migrate this to use a builtin resource mechanism once we have | |
| 76 // one. | |
| 77 | |
| 78 // NOTE: Please re-run tools/create_bootstrap.dart on any modification of | |
| 79 // this bootstrap string. | |
| 
vsm
2013/06/11 16:17:44
I just landed my CL with interop.js.  Shall we mov
 
alexandre.ardhuin
2013/06/12 21:29:19
Done.
 | |
| 80 final _JS_BOOTSTRAP = r""" | |
| 81 (function() { | |
| 82 // Proxy support for js.dart. | |
| 83 | |
| 84 var globalContext = window; | |
| 85 | |
| 86 // Support for binding the receiver (this) in proxied functions. | |
| 87 function bindIfFunction(f, _this) { | |
| 88 if (typeof(f) != "function") { | |
| 89 return f; | |
| 90 } else { | |
| 91 return new BoundFunction(_this, f); | |
| 92 } | |
| 93 } | |
| 94 | |
| 95 function unbind(obj) { | |
| 96 if (obj instanceof BoundFunction) { | |
| 97 return obj.object; | |
| 98 } else { | |
| 99 return obj; | |
| 100 } | |
| 101 } | |
| 102 | |
| 103 function BoundFunction(_this, object) { | |
| 104 this._this = _this; | |
| 105 this.object = object; | |
| 106 } | |
| 107 | |
| 108 // Table for local objects and functions that are proxied. | |
| 109 function ProxiedObjectTable() { | |
| 110 // Name for debugging. | |
| 111 this.name = 'js-ref'; | |
| 112 | |
| 113 // Table from IDs to JS objects. | |
| 114 this.map = {}; | |
| 115 | |
| 116 // Generator for new IDs. | |
| 117 this._nextId = 0; | |
| 118 | |
| 119 // Counter for deleted proxies. | |
| 120 this._deletedCount = 0; | |
| 121 | |
| 122 // Flag for one-time initialization. | |
| 123 this._initialized = false; | |
| 124 | |
| 125 // Ports for managing communication to proxies. | |
| 126 this.port = new ReceivePortSync(); | |
| 127 this.sendPort = this.port.toSendPort(); | |
| 128 | |
| 129 // Set of IDs that are global. | |
| 130 // These will not be freed on an exitScope(). | |
| 131 this.globalIds = {}; | |
| 132 | |
| 133 // Stack of scoped handles. | |
| 134 this.handleStack = []; | |
| 135 | |
| 136 // Stack of active scopes where each value is represented by the size of | |
| 137 // the handleStack at the beginning of the scope. When an active scope | |
| 138 // is popped, the handleStack is restored to where it was when the | |
| 139 // scope was entered. | |
| 140 this.scopeIndices = []; | |
| 141 } | |
| 142 | |
| 143 // Number of valid IDs. This is the number of objects (global and local) | |
| 144 // kept alive by this table. | |
| 145 ProxiedObjectTable.prototype.count = function () { | |
| 146 return Object.keys(this.map).length; | |
| 147 } | |
| 148 | |
| 149 // Number of total IDs ever allocated. | |
| 150 ProxiedObjectTable.prototype.total = function () { | |
| 151 return this.count() + this._deletedCount; | |
| 152 } | |
| 153 | |
| 154 // Adds an object to the table and return an ID for serialization. | |
| 155 ProxiedObjectTable.prototype.add = function (obj) { | |
| 156 if (this.scopeIndices.length == 0) { | |
| 157 throw "Cannot allocate a proxy outside of a scope."; | |
| 158 } | |
| 159 // TODO(vsm): Cache refs for each obj? | |
| 160 var ref = this.name + '-' + this._nextId++; | |
| 161 this.handleStack.push(ref); | |
| 162 this.map[ref] = obj; | |
| 163 return ref; | |
| 164 } | |
| 165 | |
| 166 ProxiedObjectTable.prototype._initializeOnce = function () { | |
| 167 if (!this._initialized) { | |
| 168 this._initialize(); | |
| 169 this._initialized = true; | |
| 170 } | |
| 171 } | |
| 172 | |
| 173 // Enters a new scope for this table. | |
| 174 ProxiedObjectTable.prototype.enterScope = function() { | |
| 175 this._initializeOnce(); | |
| 176 this.scopeIndices.push(this.handleStack.length); | |
| 177 } | |
| 178 | |
| 179 // Invalidates all non-global IDs in the current scope and | |
| 180 // exit the current scope. | |
| 181 ProxiedObjectTable.prototype.exitScope = function() { | |
| 182 var start = this.scopeIndices.pop(); | |
| 183 for (var i = start; i < this.handleStack.length; ++i) { | |
| 184 var key = this.handleStack[i]; | |
| 185 if (!this.globalIds.hasOwnProperty(key)) { | |
| 186 delete this.map[this.handleStack[i]]; | |
| 187 this._deletedCount++; | |
| 188 } | |
| 189 } | |
| 190 this.handleStack = this.handleStack.splice(0, start); | |
| 191 } | |
| 192 | |
| 193 // Makes this ID globally scope. It must be explicitly invalidated. | |
| 194 ProxiedObjectTable.prototype.globalize = function(id) { | |
| 195 this.globalIds[id] = true; | |
| 196 } | |
| 197 | |
| 198 // Invalidates this ID, potentially freeing its corresponding object. | |
| 199 ProxiedObjectTable.prototype.invalidate = function(id) { | |
| 200 var old = this.get(id); | |
| 201 delete this.globalIds[id]; | |
| 202 delete this.map[id]; | |
| 203 this._deletedCount++; | |
| 204 } | |
| 205 | |
| 206 // Gets the object or function corresponding to this ID. | |
| 207 ProxiedObjectTable.prototype.get = function (id) { | |
| 208 if (!this.map.hasOwnProperty(id)) { | |
| 209 throw 'Proxy ' + id + ' has been invalidated.' | |
| 210 } | |
| 211 return this.map[id]; | |
| 212 } | |
| 213 | |
| 214 ProxiedObjectTable.prototype._initialize = function () { | |
| 215 // Configure this table's port to forward methods, getters, and setters | |
| 216 // from the remote proxy to the local object. | |
| 217 var table = this; | |
| 218 | |
| 219 this.port.receive(function (message) { | |
| 220 // TODO(vsm): Support a mechanism to register a handler here. | |
| 221 try { | |
| 222 var object = table.get(message[0]); | |
| 223 var receiver = unbind(object); | |
| 224 var member = message[1]; | |
| 225 var kind = message[2]; | |
| 226 var args = message[3].map(deserialize); | |
| 227 if (kind == 'get') { | |
| 228 // Getter. | |
| 229 var field = member; | |
| 230 if (field in receiver && args.length == 0) { | |
| 231 var result = bindIfFunction(receiver[field], receiver); | |
| 232 return [ 'return', serialize(result) ]; | |
| 233 } | |
| 234 } else if (kind == 'set') { | |
| 235 // Setter. | |
| 236 var field = member; | |
| 237 if (args.length == 1) { | |
| 238 return [ 'return', serialize(receiver[field] = args[0]) ]; | |
| 239 } | |
| 240 } else if (kind == 'hasProperty') { | |
| 241 var field = member; | |
| 242 return [ 'return', field in receiver ]; | |
| 243 } else if (kind == 'apply') { | |
| 244 // Direct function invocation. | |
| 245 return [ 'return', serialize(receiver.apply(args[0], args.slice(1))) ] ; | |
| 246 } else if (member == '[]' && args.length == 1) { | |
| 247 // Index getter. | |
| 248 var result = bindIfFunction(receiver[args[0]], receiver); | |
| 249 return [ 'return', serialize(result) ]; | |
| 250 } else if (member == '[]=' && args.length == 2) { | |
| 251 // Index setter. | |
| 252 return [ 'return', serialize(receiver[args[0]] = args[1]) ]; | |
| 253 } else { | |
| 254 // Member function invocation. | |
| 255 var f = receiver[member]; | |
| 256 if (f) { | |
| 257 var result = f.apply(receiver, args); | |
| 258 return [ 'return', serialize(result) ]; | |
| 259 } | |
| 260 } | |
| 261 return [ 'none' ]; | |
| 262 } catch (e) { | |
| 263 return [ 'throws', e.toString() ]; | |
| 264 } | |
| 265 }); | |
| 266 } | |
| 267 | |
| 268 // Singleton for local proxied objects. | |
| 269 var proxiedObjectTable = new ProxiedObjectTable(); | |
| 270 | |
| 271 // Type for remote proxies to Dart objects. | |
| 272 function DartProxy(id, sendPort) { | |
| 273 this.id = id; | |
| 274 this.port = sendPort; | |
| 275 } | |
| 276 | |
| 277 // Serializes JS types to SendPortSync format: | |
| 278 // - primitives -> primitives | |
| 279 // - sendport -> sendport | |
| 280 // - Function -> [ 'funcref', function-id, sendport ] | |
| 281 // - Object -> [ 'objref', object-id, sendport ] | |
| 282 function serialize(message) { | |
| 283 if (message == null) { | |
| 284 return null; // Convert undefined to null. | |
| 285 } else if (typeof(message) == 'string' || | |
| 286 typeof(message) == 'number' || | |
| 287 typeof(message) == 'boolean') { | |
| 288 // Primitives are passed directly through. | |
| 289 return message; | |
| 290 } else if (message instanceof SendPortSync) { | |
| 291 // Non-proxied objects are serialized. | |
| 292 return message; | |
| 293 } else if (message instanceof BoundFunction && | |
| 294 typeof(message.object) == 'function') { | |
| 295 // Local function proxy. | |
| 296 return [ 'funcref', | |
| 297 proxiedObjectTable.add(message), | |
| 298 proxiedObjectTable.sendPort ]; | |
| 299 } else if (typeof(message) == 'function') { | |
| 300 if ('_dart_id' in message) { | |
| 301 // Remote function proxy. | |
| 302 var remoteId = message._dart_id; | |
| 303 var remoteSendPort = message._dart_port; | |
| 304 return [ 'funcref', remoteId, remoteSendPort ]; | |
| 305 } else { | |
| 306 // Local function proxy. | |
| 307 return [ 'funcref', | |
| 308 proxiedObjectTable.add(message), | |
| 309 proxiedObjectTable.sendPort ]; | |
| 310 } | |
| 311 } else if (message instanceof DartProxy) { | |
| 312 // Remote object proxy. | |
| 313 return [ 'objref', message.id, message.port ]; | |
| 314 } else { | |
| 315 // Local object proxy. | |
| 316 return [ 'objref', | |
| 317 proxiedObjectTable.add(message), | |
| 318 proxiedObjectTable.sendPort ]; | |
| 319 } | |
| 320 } | |
| 321 | |
| 322 function deserialize(message) { | |
| 323 if (message == null) { | |
| 324 return null; // Convert undefined to null. | |
| 325 } else if (typeof(message) == 'string' || | |
| 326 typeof(message) == 'number' || | |
| 327 typeof(message) == 'boolean') { | |
| 328 // Primitives are passed directly through. | |
| 329 return message; | |
| 330 } else if (message instanceof SendPortSync) { | |
| 331 // Serialized type. | |
| 332 return message; | |
| 333 } | |
| 334 var tag = message[0]; | |
| 335 switch (tag) { | |
| 336 case 'funcref': return deserializeFunction(message); | |
| 337 case 'objref': return deserializeObject(message); | |
| 338 } | |
| 339 throw 'Unsupported serialized data: ' + message; | |
| 340 } | |
| 341 | |
| 342 // Create a local function that forwards to the remote function. | |
| 343 function deserializeFunction(message) { | |
| 344 var id = message[1]; | |
| 345 var port = message[2]; | |
| 346 // TODO(vsm): Add a more robust check for a local SendPortSync. | |
| 347 if ("receivePort" in port) { | |
| 348 // Local function. | |
| 349 return unbind(proxiedObjectTable.get(id)); | |
| 350 } else { | |
| 351 // Remote function. Forward to its port. | |
| 352 var f = function () { | |
| 353 var depth = enterScope(); | |
| 354 try { | |
| 355 var args = Array.prototype.slice.apply(arguments); | |
| 356 args.splice(0, 0, this); | |
| 357 args = args.map(serialize); | |
| 358 var result = port.callSync([id, '#call', args]); | |
| 359 if (result[0] == 'throws') throw deserialize(result[1]); | |
| 360 return deserialize(result[1]); | |
| 361 } finally { | |
| 362 exitScope(depth); | |
| 363 } | |
| 364 }; | |
| 365 // Cache the remote id and port. | |
| 366 f._dart_id = id; | |
| 367 f._dart_port = port; | |
| 368 return f; | |
| 369 } | |
| 370 } | |
| 371 | |
| 372 // Creates a DartProxy to forwards to the remote object. | |
| 373 function deserializeObject(message) { | |
| 374 var id = message[1]; | |
| 375 var port = message[2]; | |
| 376 // TODO(vsm): Add a more robust check for a local SendPortSync. | |
| 377 if ("receivePort" in port) { | |
| 378 // Local object. | |
| 379 return proxiedObjectTable.get(id); | |
| 380 } else { | |
| 381 // Remote object. | |
| 382 return new DartProxy(id, port); | |
| 383 } | |
| 384 } | |
| 385 | |
| 386 // Remote handler to construct a new JavaScript object given its | |
| 387 // serialized constructor and arguments. | |
| 388 function construct(args) { | |
| 389 args = args.map(deserialize); | |
| 390 var constructor = unbind(args[0]); | |
| 391 args = Array.prototype.slice.call(args, 1); | |
| 392 | |
| 393 // Until 10 args, the 'new' operator is used. With more arguments we use a | |
| 394 // generic way that may not work, particularly when the constructor does not | |
| 395 // have an "apply" method. | |
| 396 var ret = null; | |
| 397 if (args.length === 0) { | |
| 398 ret = new constructor(); | |
| 399 } else if (args.length === 1) { | |
| 400 ret = new constructor(args[0]); | |
| 401 } else if (args.length === 2) { | |
| 402 ret = new constructor(args[0], args[1]); | |
| 403 } else if (args.length === 3) { | |
| 404 ret = new constructor(args[0], args[1], args[2]); | |
| 405 } else if (args.length === 4) { | |
| 406 ret = new constructor(args[0], args[1], args[2], args[3]); | |
| 407 } else if (args.length === 5) { | |
| 408 ret = new constructor(args[0], args[1], args[2], args[3], args[4]); | |
| 409 } else if (args.length === 6) { | |
| 410 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
| 411 args[5]); | |
| 412 } else if (args.length === 7) { | |
| 413 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
| 414 args[5], args[6]); | |
| 415 } else if (args.length === 8) { | |
| 416 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
| 417 args[5], args[6], args[7]); | |
| 418 } else if (args.length === 9) { | |
| 419 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
| 420 args[5], args[6], args[7], args[8]); | |
| 421 } else if (args.length === 10) { | |
| 422 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
| 423 args[5], args[6], args[7], args[8], args[9]); | |
| 424 } else { | |
| 425 // Dummy Type with correct constructor. | |
| 426 var Type = function(){}; | |
| 427 Type.prototype = constructor.prototype; | |
| 428 | |
| 429 // Create a new instance | |
| 430 var instance = new Type(); | |
| 431 | |
| 432 // Call the original constructor. | |
| 433 ret = constructor.apply(instance, args); | |
| 434 ret = Object(ret) === ret ? ret : instance; | |
| 435 } | |
| 436 return serialize(ret); | |
| 437 } | |
| 438 | |
| 439 // Remote handler to return the top-level JavaScript context. | |
| 440 function context(data) { | |
| 441 return serialize(globalContext); | |
| 442 } | |
| 443 | |
| 444 // Remote handler to track number of live / allocated proxies. | |
| 445 function proxyCount() { | |
| 446 var live = proxiedObjectTable.count(); | |
| 447 var total = proxiedObjectTable.total(); | |
| 448 return [live, total]; | |
| 449 } | |
| 450 | |
| 451 // Return true if two JavaScript proxies are equal (==). | |
| 452 function proxyEquals(args) { | |
| 453 return deserialize(args[0]) == deserialize(args[1]); | |
| 454 } | |
| 455 | |
| 456 // Return true if a JavaScript proxy is instance of a given type (instanceof). | |
| 457 function proxyInstanceof(args) { | |
| 458 var obj = unbind(deserialize(args[0])); | |
| 459 var type = unbind(deserialize(args[1])); | |
| 460 return obj instanceof type; | |
| 461 } | |
| 462 | |
| 463 // Return true if a JavaScript proxy is instance of a given type (instanceof). | |
| 464 function proxyDeleteProperty(args) { | |
| 465 var obj = unbind(deserialize(args[0])); | |
| 466 var member = unbind(deserialize(args[1])); | |
| 467 delete obj[member]; | |
| 468 } | |
| 469 | |
| 470 function proxyConvert(args) { | |
| 471 return serialize(deserializeDataTree(args)); | |
| 472 } | |
| 473 | |
| 474 function deserializeDataTree(data) { | |
| 475 var type = data[0]; | |
| 476 var value = data[1]; | |
| 477 if (type === 'map') { | |
| 478 var obj = {}; | |
| 479 for (var i = 0; i < value.length; i++) { | |
| 480 obj[value[i][0]] = deserializeDataTree(value[i][1]); | |
| 481 } | |
| 482 return obj; | |
| 483 } else if (type === 'list') { | |
| 484 var list = []; | |
| 485 for (var i = 0; i < value.length; i++) { | |
| 486 list.push(deserializeDataTree(value[i])); | |
| 487 } | |
| 488 return list; | |
| 489 } else /* 'simple' */ { | |
| 490 return deserialize(value); | |
| 491 } | |
| 492 } | |
| 493 | |
| 494 function makeGlobalPort(name, f) { | |
| 495 var port = new ReceivePortSync(); | |
| 496 port.receive(f); | |
| 497 window.registerPort(name, port.toSendPort()); | |
| 498 } | |
| 499 | |
| 500 // Enters a new scope in the JavaScript context. | |
| 501 function enterJavaScriptScope() { | |
| 502 proxiedObjectTable.enterScope(); | |
| 503 } | |
| 504 | |
| 505 // Enters a new scope in both the JavaScript and Dart context. | |
| 506 var _dartEnterScopePort = null; | |
| 507 function enterScope() { | |
| 508 enterJavaScriptScope(); | |
| 509 if (!_dartEnterScopePort) { | |
| 510 _dartEnterScopePort = window.lookupPort('js-dart-enter-scope'); | |
| 511 } | |
| 512 return _dartEnterScopePort.callSync([]); | |
| 513 } | |
| 514 | |
| 515 // Exits the current scope (and invalidate local IDs) in the JavaScript | |
| 516 // context. | |
| 517 function exitJavaScriptScope() { | |
| 518 proxiedObjectTable.exitScope(); | |
| 519 } | |
| 520 | |
| 521 // Exits the current scope in both the JavaScript and Dart context. | |
| 522 var _dartExitScopePort = null; | |
| 523 function exitScope(depth) { | |
| 524 exitJavaScriptScope(); | |
| 525 if (!_dartExitScopePort) { | |
| 526 _dartExitScopePort = window.lookupPort('js-dart-exit-scope'); | |
| 527 } | |
| 528 return _dartExitScopePort.callSync([ depth ]); | |
| 529 } | |
| 530 | |
| 531 makeGlobalPort('dart-js-context', context); | |
| 532 makeGlobalPort('dart-js-create', construct); | |
| 533 makeGlobalPort('dart-js-proxy-count', proxyCount); | |
| 534 makeGlobalPort('dart-js-equals', proxyEquals); | |
| 535 makeGlobalPort('dart-js-instanceof', proxyInstanceof); | |
| 536 makeGlobalPort('dart-js-delete-property', proxyDeleteProperty); | |
| 537 makeGlobalPort('dart-js-convert', proxyConvert); | |
| 538 makeGlobalPort('dart-js-enter-scope', enterJavaScriptScope); | |
| 539 makeGlobalPort('dart-js-exit-scope', exitJavaScriptScope); | |
| 540 makeGlobalPort('dart-js-globalize', function(data) { | |
| 541 if (data[0] == "objref" || data[0] == "funcref") { | |
| 542 return proxiedObjectTable.globalize(data[1]); | |
| 543 } | |
| 544 throw 'Illegal type: ' + data[0]; | |
| 545 }); | |
| 546 makeGlobalPort('dart-js-invalidate', function(data) { | |
| 547 if (data[0] == "objref" || data[0] == "funcref") { | |
| 548 return proxiedObjectTable.invalidate(data[1]); | |
| 549 } | |
| 550 throw 'Illegal type: ' + data[0]; | |
| 551 }); | |
| 552 })(); | |
| 553 """; | |
| 554 | |
| 555 // Injects JavaScript source code onto the page. | |
| 556 // This is only used to load the bootstrapping code above. | |
| 557 void _inject(code) { | |
| 
vsm
2013/06/11 16:17:44
We can also eliminate this.
 
alexandre.ardhuin
2013/06/12 21:29:19
Done.
 | |
| 558 final script = new ScriptElement(); | |
| 559 script.type = 'text/javascript'; | |
| 560 script.innerHtml = code; | |
| 561 document.body.nodes.add(script); | |
| 562 } | |
| 563 | |
| 564 // Global ports to manage communication from Dart to JS. | |
| 565 SendPortSync _jsPortSync = null; | |
| 566 SendPortSync _jsPortCreate = null; | |
| 567 SendPortSync _jsPortProxyCount = null; | |
| 568 SendPortSync _jsPortEquals = null; | |
| 569 SendPortSync _jsPortInstanceof = null; | |
| 570 SendPortSync _jsPortDeleteProperty = null; | |
| 571 SendPortSync _jsPortConvert = null; | |
| 572 SendPortSync _jsEnterJavaScriptScope = null; | |
| 573 SendPortSync _jsExitJavaScriptScope = null; | |
| 574 SendPortSync _jsGlobalize = null; | |
| 575 SendPortSync _jsInvalidate = null; | |
| 576 | |
| 577 // Global ports to manage communication from JS to Dart. | |
| 578 ReceivePortSync _dartEnterDartScope = null; | |
| 579 ReceivePortSync _dartExitDartScope = null; | |
| 580 | |
| 581 // Initializes bootstrap code and ports. | |
| 582 void _initialize() { | |
| 583 if (_jsPortSync != null) return; | |
| 584 | |
| 585 // Test if the port is already defined. | |
| 586 try { | |
| 587 _jsPortSync = window.lookupPort('dart-js-context'); | |
| 588 } catch (e) { | |
| 589 // TODO(vsm): Suppress the exception until dartbug.com/5854 is fixed. | |
| 590 } | |
| 591 | |
| 592 // If not, try injecting the script. | |
| 593 if (_jsPortSync == null) { | |
| 594 _inject(_JS_BOOTSTRAP); | |
| 595 _jsPortSync = window.lookupPort('dart-js-context'); | |
| 596 } | |
| 597 | |
| 598 _jsPortCreate = window.lookupPort('dart-js-create'); | |
| 599 _jsPortProxyCount = window.lookupPort('dart-js-proxy-count'); | |
| 600 _jsPortEquals = window.lookupPort('dart-js-equals'); | |
| 601 _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); | |
| 602 _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property'); | |
| 603 _jsPortConvert = window.lookupPort('dart-js-convert'); | |
| 604 _jsEnterJavaScriptScope = window.lookupPort('dart-js-enter-scope'); | |
| 605 _jsExitJavaScriptScope = window.lookupPort('dart-js-exit-scope'); | |
| 606 _jsGlobalize = window.lookupPort('dart-js-globalize'); | |
| 607 _jsInvalidate = window.lookupPort('dart-js-invalidate'); | |
| 608 | |
| 609 _dartEnterDartScope = new ReceivePortSync() | |
| 610 ..receive((_) => _enterScope()); | |
| 611 _dartExitDartScope = new ReceivePortSync() | |
| 612 ..receive((args) => _exitScope(args[0])); | |
| 613 window.registerPort('js-dart-enter-scope', _dartEnterDartScope.toSendPort()); | |
| 614 window.registerPort('js-dart-exit-scope', _dartExitDartScope.toSendPort()); | |
| 615 } | |
| 616 | |
| 617 /** | |
| 618 * Returns a proxy to the global JavaScript context for this page. | |
| 619 */ | |
| 620 JsObject get context { | |
| 621 _enterScopeIfNeeded(); | |
| 622 return _deserialize(_jsPortSync.callSync([])); | |
| 623 } | |
| 624 | |
| 625 // Depth of current scope. Return 0 if no scope. | |
| 626 get _depth => _proxiedObjectTable._scopeIndices.length; | |
| 627 | |
| 628 // If we are not already in a scope, enter one and register a | |
| 629 // corresponding exit once we return to the event loop. | |
| 630 void _enterScopeIfNeeded() { | |
| 631 if (_depth == 0) { | |
| 632 var depth = _enterScope(); | |
| 633 runAsync(() => _exitScope(depth)); | |
| 634 } | |
| 635 } | |
| 636 | |
| 637 /** | |
| 638 * Executes the closure [f] within a scope. Any proxies created within this | |
| 639 * scope are invalidated afterward unless they are converted to a global proxy. | |
| 640 */ | |
| 641 scoped(f) { | |
| 642 var depth = _enterScope(); | |
| 643 try { | |
| 644 return f(); | |
| 645 } finally { | |
| 646 _exitScope(depth); | |
| 647 } | |
| 648 } | |
| 649 | |
| 650 int _enterScope() { | |
| 651 _initialize(); | |
| 652 _proxiedObjectTable.enterScope(); | |
| 653 _jsEnterJavaScriptScope.callSync([]); | |
| 654 return _proxiedObjectTable._scopeIndices.length; | |
| 655 } | |
| 656 | |
| 657 void _exitScope(int depth) { | |
| 658 assert(_proxiedObjectTable._scopeIndices.length == depth); | |
| 659 _jsExitJavaScriptScope.callSync([]); | |
| 660 _proxiedObjectTable.exitScope(); | |
| 661 } | |
| 662 | |
| 663 /** | |
| 664 * Retains the given [object] beyond the current scope. | |
| 665 * Instead, it will need to be explicitly released. | |
| 666 * The given [object] is returned for convenience. | |
| 667 */ | |
| 668 // TODO(aa) : change dynamic to Serializable<Proxy> if http://dartbug.com/9023 | |
| 669 // is fixed. | |
| 670 // TODO(aa) : change to "<T extends Serializable<Proxy>> T retain(T object)" | |
| 671 // once generic methods have landed. | |
| 672 dynamic retain(Serializable<JsObject> object) { | |
| 673 _jsGlobalize.callSync(_serialize(object.toJs())); | |
| 674 return object; | |
| 675 } | |
| 676 | |
| 677 /** | |
| 678 * Releases a retained [object]. | |
| 679 */ | |
| 680 void release(Serializable<JsObject> object) { | |
| 681 _jsInvalidate.callSync(_serialize(object.toJs())); | |
| 682 } | |
| 683 | |
| 684 /** | |
| 685 * Converts a json-like [data] to a JavaScript map or array and return a | |
| 686 * [JsObject] to it. | |
| 687 */ | |
| 688 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); | |
| 689 | |
| 690 /** | |
| 691 * Converts a local Dart function to a callback that can be passed to | |
| 692 * JavaScript. | |
| 693 * | |
| 694 * A callback can either be: | |
| 695 * | |
| 696 * - single-fire, in which case it is automatically invalidated after the first | |
| 697 * invocation, or | |
| 698 * - multi-fire, in which case it must be explicitly disposed. | |
| 699 */ | |
| 700 class Callback implements Serializable<JsFunction> { | |
| 701 final bool _manualDispose; | |
| 702 JsFunction _f; | |
| 703 | |
| 704 Callback._internal(this._manualDispose, Function f, bool withThis) { | |
| 705 final id = _proxiedObjectTable.add((List args) { | |
| 706 final arguments = new List.from(args); | |
| 707 if (!withThis) arguments.removeAt(0); | |
| 708 if (_manualDispose) { | |
| 709 return Function.apply(f, arguments); | |
| 710 } else { | |
| 711 try { | |
| 712 return Function.apply(f, arguments); | |
| 713 } finally { | |
| 714 _dispose(); | |
| 715 } | |
| 716 } | |
| 717 }); | |
| 718 _proxiedObjectTable.globalize(id); | |
| 719 _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id); | |
| 720 } | |
| 721 | |
| 722 _dispose() { | |
| 723 _proxiedObjectTable.invalidate(_f._id); | |
| 724 } | |
| 725 | |
| 726 JsFunction toJs() => _f; | |
| 727 | |
| 728 /** | |
| 729 * Disposes this [Callback] so that it may be collected. | |
| 730 * Once a [Callback] is disposed, it is an error to invoke it from JavaScript. | |
| 731 */ | |
| 732 dispose() { | |
| 733 assert(_manualDispose); | |
| 734 _dispose(); | |
| 735 } | |
| 736 | |
| 737 /** | |
| 738 * Creates a single-fire [Callback] that invokes [f]. The callback is | |
| 739 * automatically disposed after the first invocation. | |
| 740 */ | |
| 741 factory Callback.once(Function f, {bool withThis: false}) => | |
| 742 new Callback._internal(false, f, withThis); | |
| 743 | |
| 744 /** | |
| 745 * Creates a multi-fire [Callback] that invokes [f]. The callback must be | |
| 746 * explicitly disposed to avoid memory leaks. | |
| 747 */ | |
| 748 factory Callback.many(Function f, {bool withThis: false}) => | |
| 749 new Callback._internal(true, f, withThis); | |
| 750 } | |
| 751 | |
| 752 /** | |
| 753 * Proxies to JavaScript objects. | |
| 754 */ | |
| 755 class JsObject implements Serializable<JsObject> { | |
| 756 final SendPortSync _port; | |
| 757 final String _id; | |
| 758 | |
| 759 /** | |
| 760 * Constructs a [JsObject] to a new JavaScript object by invoking a (proxy to | |
| 761 * a) JavaScript [constructor]. The [arguments] list should contain either | |
| 762 * primitive values, DOM elements, or Proxies. | |
| 763 */ | |
| 764 factory JsObject(Serializable<JsFunction> constructor, [List arguments]) { | |
| 765 _enterScopeIfNeeded(); | |
| 766 final params = [constructor]; | |
| 767 if (arguments != null) params.addAll(arguments); | |
| 768 final serialized = params.map(_serialize).toList(); | |
| 769 final result = _jsPortCreate.callSync(serialized); | |
| 770 return _deserialize(result); | |
| 771 } | |
| 772 | |
| 773 /** | |
| 774 * Constructs a [JsObject] to a new JavaScript map or list created defined via | |
| 775 * Dart map or list. | |
| 776 */ | |
| 777 factory JsObject._json(data) { | |
| 778 _enterScopeIfNeeded(); | |
| 779 return _convert(data); | |
| 780 } | |
| 781 | |
| 782 static _convert(data) { | |
| 783 return _deserialize(_jsPortConvert.callSync(_serializeDataTree(data))); | |
| 784 } | |
| 785 | |
| 786 static _serializeDataTree(data) { | |
| 787 if (data is Map) { | |
| 788 final entries = new List(); | |
| 789 for (var key in data.keys) { | |
| 790 entries.add([key, _serializeDataTree(data[key])]); | |
| 791 } | |
| 792 return ['map', entries]; | |
| 793 } else if (data is Iterable) { | |
| 794 return ['list', data.map(_serializeDataTree).toList()]; | |
| 795 } else { | |
| 796 return ['simple', _serialize(data)]; | |
| 797 } | |
| 798 } | |
| 799 | |
| 800 JsObject._internal(this._port, this._id); | |
| 801 | |
| 802 JsObject toJs() => this; | |
| 803 | |
| 804 // Resolve whether this is needed. | |
| 805 operator[](arg) => _forward(this, '[]', 'method', [ arg ]); | |
| 806 | |
| 807 // Resolve whether this is needed. | |
| 808 operator[]=(key, value) => _forward(this, '[]=', 'method', [ key, value ]); | |
| 809 | |
| 810 // Test if this is equivalent to another Proxy. This essentially | |
| 811 // maps to JavaScript's == operator. | |
| 812 // TODO(vsm): Can we avoid forwarding to JS? | |
| 813 operator==(other) => identical(this, other) | |
| 814 ? true | |
| 815 : (other is JsObject && | |
| 816 _jsPortEquals.callSync([_serialize(this), _serialize(other)])); | |
| 817 | |
| 818 /** | |
| 819 * Check if this [JsObject] has a [name] property. | |
| 820 */ | |
| 821 bool hasProperty(String name) => _forward(this, name, 'hasProperty', []); | |
| 822 | |
| 823 /** | |
| 824 * Delete the [name] property. | |
| 825 */ | |
| 826 void deleteProperty(String name) { | |
| 827 _jsPortDeleteProperty.callSync([this, name].map(_serialize).toList()); | |
| 828 } | |
| 829 | |
| 830 /** | |
| 831 * Check if this [JsObject] is instance of [type]. | |
| 832 */ | |
| 833 bool instanceof(Serializable<JsFunction> type) => | |
| 834 _jsPortInstanceof.callSync([this, type].map(_serialize).toList()); | |
| 835 | |
| 836 String toString() { | |
| 837 try { | |
| 838 return _forward(this, 'toString', 'method', []); | |
| 839 } catch(e) { | |
| 840 return super.toString(); | |
| 841 } | |
| 842 } | |
| 843 | |
| 844 callMethod(String name, [List args]) { | |
| 845 return _forward(this, name, 'method', args != null ? args : []); | |
| 846 } | |
| 847 | |
| 848 // Forward member accesses to the backing JavaScript object. | |
| 849 static _forward(JsObject receiver, String member, String kind, List args) { | |
| 850 _enterScopeIfNeeded(); | |
| 851 var result = receiver._port.callSync([receiver._id, member, kind, | |
| 852 args.map(_serialize).toList()]); | |
| 853 switch (result[0]) { | |
| 854 case 'return': return _deserialize(result[1]); | |
| 855 case 'throws': throw _deserialize(result[1]); | |
| 856 case 'none': throw new NoSuchMethodError(receiver, member, args, {}); | |
| 857 default: throw 'Invalid return value'; | |
| 858 } | |
| 859 } | |
| 860 } | |
| 861 | |
| 862 /// A [JsObject] subtype to JavaScript functions. | |
| 863 class JsFunction extends JsObject implements Serializable<JsFunction> { | |
| 864 JsFunction._internal(SendPortSync port, String id) | |
| 865 : super._internal(port, id); | |
| 866 | |
| 867 apply(thisArg, [List args]) { | |
| 868 return JsObject._forward(this, '', 'apply', | |
| 869 [thisArg]..addAll(args == null ? [] : args)); | |
| 870 } | |
| 871 } | |
| 872 | |
| 873 /// Marker class used to indicate it is serializable to js. If a class is a | |
| 874 /// [Serializable] the "toJs" method will be called and the result will be used | |
| 875 /// as value. | |
| 876 abstract class Serializable<T> { | |
| 877 T toJs(); | |
| 878 } | |
| 879 | |
| 880 // A table to managed local Dart objects that are proxied in JavaScript. | |
| 881 class _ProxiedObjectTable { | |
| 882 // Debugging name. | |
| 883 final String _name; | |
| 884 | |
| 885 // Generator for unique IDs. | |
| 886 int _nextId; | |
| 887 | |
| 888 // Counter for invalidated IDs for debugging. | |
| 889 int _deletedCount; | |
| 890 | |
| 891 // Table of IDs to Dart objects. | |
| 892 final Map<String, Object> _registry; | |
| 893 | |
| 894 // Port to handle and forward requests to the underlying Dart objects. | |
| 895 // A remote proxy is uniquely identified by an ID and SendPortSync. | |
| 896 final ReceivePortSync _port; | |
| 897 | |
| 898 // The set of IDs that are global. These must be explicitly invalidated. | |
| 899 final Set<String> _globalIds; | |
| 900 | |
| 901 // The stack of valid IDs. | |
| 902 final List<String> _handleStack; | |
| 903 | |
| 904 // The stack of scopes, where each scope is represented by an index into the | |
| 905 // handleStack. | |
| 906 final List<int> _scopeIndices; | |
| 907 | |
| 908 // Enters a new scope. | |
| 909 enterScope() { | |
| 910 _scopeIndices.add(_handleStack.length); | |
| 911 } | |
| 912 | |
| 913 // Invalidates non-global IDs created in the current scope and | |
| 914 // restore to the previous scope. | |
| 915 exitScope() { | |
| 916 int start = _scopeIndices.removeLast(); | |
| 917 for (int i = start; i < _handleStack.length; ++i) { | |
| 918 String key = _handleStack[i]; | |
| 919 if (!_globalIds.contains(key)) { | |
| 920 _registry.remove(_handleStack[i]); | |
| 921 _deletedCount++; | |
| 922 } | |
| 923 } | |
| 924 if (start != _handleStack.length) { | |
| 925 _handleStack.removeRange(start, _handleStack.length - start); | |
| 926 } | |
| 927 } | |
| 928 | |
| 929 // Converts an ID to a global. | |
| 930 globalize(id) => _globalIds.add(id); | |
| 931 | |
| 932 // Invalidates an ID. | |
| 933 invalidate(id) { | |
| 934 var old = _registry[id]; | |
| 935 _globalIds.remove(id); | |
| 936 _registry.remove(id); | |
| 937 _deletedCount++; | |
| 938 return old; | |
| 939 } | |
| 940 | |
| 941 // Replaces the object referenced by an ID. | |
| 942 _replace(id, x) { | |
| 943 _registry[id] = x; | |
| 944 } | |
| 945 | |
| 946 _ProxiedObjectTable() : | |
| 947 _name = 'dart-ref', | |
| 948 _nextId = 0, | |
| 949 _deletedCount = 0, | |
| 950 _registry = {}, | |
| 951 _port = new ReceivePortSync(), | |
| 952 _handleStack = new List<String>(), | |
| 953 _scopeIndices = new List<int>(), | |
| 954 _globalIds = new Set<String>() { | |
| 955 _port.receive((msg) { | |
| 956 try { | |
| 957 final receiver = _registry[msg[0]]; | |
| 958 final method = msg[1]; | |
| 959 final args = msg[2].map(_deserialize).toList(); | |
| 960 if (method == '#call') { | |
| 961 final func = receiver as Function; | |
| 962 var result = _serialize(func(args)); | |
| 963 return ['return', result]; | |
| 964 } else { | |
| 965 // TODO(vsm): Support a mechanism to register a handler here. | |
| 966 throw 'Invocation unsupported on non-function Dart proxies'; | |
| 967 } | |
| 968 } catch (e) { | |
| 969 // TODO(vsm): callSync should just handle exceptions itself. | |
| 970 return ['throws', '$e']; | |
| 971 } | |
| 972 }); | |
| 973 } | |
| 974 | |
| 975 // Adds a new object to the table and return a new ID for it. | |
| 976 String add(x) { | |
| 977 _enterScopeIfNeeded(); | |
| 978 // TODO(vsm): Cache x and reuse id. | |
| 979 final id = '$_name-${_nextId++}'; | |
| 980 _registry[id] = x; | |
| 981 _handleStack.add(id); | |
| 982 return id; | |
| 983 } | |
| 984 | |
| 985 // Gets an object by ID. | |
| 986 Object get(String id) { | |
| 987 return _registry[id]; | |
| 988 } | |
| 989 | |
| 990 // Gets the current number of objects kept alive by this table. | |
| 991 get count => _registry.length; | |
| 992 | |
| 993 // Gets the total number of IDs ever allocated. | |
| 994 get total => count + _deletedCount; | |
| 995 | |
| 996 // Gets a send port for this table. | |
| 997 get sendPort => _port.toSendPort(); | |
| 998 } | |
| 999 | |
| 1000 // The singleton to manage proxied Dart objects. | |
| 1001 _ProxiedObjectTable _proxiedObjectTable = new _ProxiedObjectTable(); | |
| 1002 | |
| 1003 /// End of proxy implementation. | |
| 1004 | |
| 1005 // Dart serialization support. | |
| 1006 | |
| 1007 _serialize(var message) { | |
| 1008 if (message == null) { | |
| 1009 return null; // Convert undefined to null. | |
| 1010 } else if (message is String || | |
| 1011 message is num || | |
| 1012 message is bool) { | |
| 1013 // Primitives are passed directly through. | |
| 1014 return message; | |
| 1015 } else if (message is SendPortSync) { | |
| 1016 // Non-proxied objects are serialized. | |
| 1017 return message; | |
| 1018 } else if (message is JsFunction) { | |
| 1019 // Remote function proxy. | |
| 1020 return [ 'funcref', message._id, message._port ]; | |
| 1021 } else if (message is JsObject) { | |
| 1022 // Remote object proxy. | |
| 1023 return [ 'objref', message._id, message._port ]; | |
| 1024 } else if (message is Serializable) { | |
| 1025 // use of result of toJs() | |
| 1026 return _serialize(message.toJs()); | |
| 1027 } else { | |
| 1028 // Local object proxy. | |
| 1029 return [ 'objref', | |
| 1030 _proxiedObjectTable.add(message), | |
| 1031 _proxiedObjectTable.sendPort ]; | |
| 1032 } | |
| 1033 } | |
| 1034 | |
| 1035 _deserialize(var message) { | |
| 1036 deserializeFunction(message) { | |
| 1037 var id = message[1]; | |
| 1038 var port = message[2]; | |
| 1039 if (port == _proxiedObjectTable.sendPort) { | |
| 1040 // Local function. | |
| 1041 return _proxiedObjectTable.get(id); | |
| 1042 } else { | |
| 1043 // Remote function. Forward to its port. | |
| 1044 return new JsFunction._internal(port, id); | |
| 1045 } | |
| 1046 } | |
| 1047 | |
| 1048 deserializeObject(message) { | |
| 1049 var id = message[1]; | |
| 1050 var port = message[2]; | |
| 1051 if (port == _proxiedObjectTable.sendPort) { | |
| 1052 // Local object. | |
| 1053 return _proxiedObjectTable.get(id); | |
| 1054 } else { | |
| 1055 // Remote object. | |
| 1056 return new JsObject._internal(port, id); | |
| 1057 } | |
| 1058 } | |
| 1059 | |
| 1060 if (message == null) { | |
| 1061 return null; // Convert undefined to null. | |
| 1062 } else if (message is String || | |
| 1063 message is num || | |
| 1064 message is bool) { | |
| 1065 // Primitives are passed directly through. | |
| 1066 return message; | |
| 1067 } else if (message is SendPortSync) { | |
| 1068 // Serialized type. | |
| 1069 return message; | |
| 1070 } | |
| 1071 var tag = message[0]; | |
| 1072 switch (tag) { | |
| 1073 case 'funcref': return deserializeFunction(message); | |
| 1074 case 'objref': return deserializeObject(message); | |
| 1075 } | |
| 1076 throw 'Unsupported serialized data: $message'; | |
| 1077 } | |
| 1078 | |
| 1079 // Fetch the number of proxies to JavaScript objects. | |
| 1080 // This returns a 2 element list. The first is the number of currently | |
| 1081 // live proxies. The second is the total number of proxies ever | |
| 1082 // allocated. | |
| 1083 List _proxyCountJavaScript() => _jsPortProxyCount.callSync([]); | |
| 1084 | |
| 1085 /** | |
| 1086 * Returns the number of allocated proxy objects matching the given | |
| 1087 * conditions. By default, the total number of live proxy objects are | |
| 1088 * return. In a well behaved program, this should stay below a small | |
| 1089 * bound. | |
| 1090 * | |
| 1091 * Set [all] to true to return the total number of proxies ever allocated. | |
| 1092 * Set [dartOnly] to only count proxies to Dart objects (live or all). | |
| 1093 * Set [jsOnly] to only count proxies to JavaScript objects (live or all). | |
| 1094 */ | |
| 1095 int proxyCount({all: false, dartOnly: false, jsOnly: false}) { | |
| 1096 final js = !dartOnly; | |
| 1097 final dart = !jsOnly; | |
| 1098 final jsCounts = js ? _proxyCountJavaScript() : null; | |
| 1099 var sum = 0; | |
| 1100 if (!all) { | |
| 1101 if (js) | |
| 1102 sum += jsCounts[0]; | |
| 1103 if (dart) | |
| 1104 sum += _proxiedObjectTable.count; | |
| 1105 } else { | |
| 1106 if (js) | |
| 1107 sum += jsCounts[1]; | |
| 1108 if (dart) | |
| 1109 sum += _proxiedObjectTable.total; | |
| 1110 } | |
| 1111 return sum; | |
| 1112 } | |
| 1113 | |
| 1114 // Prints the number of live handles in Dart and JavaScript. This is for | |
| 1115 // debugging / profiling purposes. | |
| 1116 void _proxyDebug([String message = '']) { | |
| 1117 print('Proxy status $message:'); | |
| 1118 var dartLive = proxyCount(dartOnly: true); | |
| 1119 var dartTotal = proxyCount(dartOnly: true, all: true); | |
| 1120 var jsLive = proxyCount(jsOnly: true); | |
| 1121 var jsTotal = proxyCount(jsOnly: true, all: true); | |
| 1122 print(' Dart objects Live : $dartLive (out of $dartTotal ever allocated).'); | |
| 1123 print(' JS objects Live : $jsLive (out of $jsTotal ever allocated).'); | |
| 1124 } | |
| OLD | NEW |