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/05/29 04:55:39
We'll need an alternate solution here.
alexandre.ardhuin
2013/05/31 19:11:13
Agree but I have no answer :/
Perhaps in a package
| |
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 getBoundThis(obj) { | |
104 if (obj instanceof BoundFunction) { | |
105 return obj._this; | |
106 } else { | |
107 return globalContext; | |
108 } | |
109 } | |
110 | |
111 function BoundFunction(_this, object) { | |
112 this._this = _this; | |
113 this.object = object; | |
114 } | |
115 | |
116 // Table for local objects and functions that are proxied. | |
117 function ProxiedObjectTable() { | |
118 // Name for debugging. | |
119 this.name = 'js-ref'; | |
120 | |
121 // Table from IDs to JS objects. | |
122 this.map = {}; | |
123 | |
124 // Generator for new IDs. | |
125 this._nextId = 0; | |
126 | |
127 // Counter for deleted proxies. | |
128 this._deletedCount = 0; | |
129 | |
130 // Flag for one-time initialization. | |
131 this._initialized = false; | |
132 | |
133 // Ports for managing communication to proxies. | |
134 this.port = new ReceivePortSync(); | |
135 this.sendPort = this.port.toSendPort(); | |
136 | |
137 // Set of IDs that are global. | |
138 // These will not be freed on an exitScope(). | |
139 this.globalIds = {}; | |
140 | |
141 // Stack of scoped handles. | |
142 this.handleStack = []; | |
143 | |
144 // Stack of active scopes where each value is represented by the size of | |
145 // the handleStack at the beginning of the scope. When an active scope | |
146 // is popped, the handleStack is restored to where it was when the | |
147 // scope was entered. | |
148 this.scopeIndices = []; | |
149 } | |
150 | |
151 // Number of valid IDs. This is the number of objects (global and local) | |
152 // kept alive by this table. | |
153 ProxiedObjectTable.prototype.count = function () { | |
154 return Object.keys(this.map).length; | |
155 } | |
156 | |
157 // Number of total IDs ever allocated. | |
158 ProxiedObjectTable.prototype.total = function () { | |
159 return this.count() + this._deletedCount; | |
160 } | |
161 | |
162 // Adds an object to the table and return an ID for serialization. | |
163 ProxiedObjectTable.prototype.add = function (obj) { | |
164 if (this.scopeIndices.length == 0) { | |
165 throw "Cannot allocate a proxy outside of a scope."; | |
166 } | |
167 // TODO(vsm): Cache refs for each obj? | |
168 var ref = this.name + '-' + this._nextId++; | |
169 this.handleStack.push(ref); | |
170 this.map[ref] = obj; | |
171 return ref; | |
172 } | |
173 | |
174 ProxiedObjectTable.prototype._initializeOnce = function () { | |
175 if (!this._initialized) { | |
176 this._initialize(); | |
177 this._initialized = true; | |
178 } | |
179 } | |
180 | |
181 // Enters a new scope for this table. | |
182 ProxiedObjectTable.prototype.enterScope = function() { | |
183 this._initializeOnce(); | |
184 this.scopeIndices.push(this.handleStack.length); | |
185 } | |
186 | |
187 // Invalidates all non-global IDs in the current scope and | |
188 // exit the current scope. | |
189 ProxiedObjectTable.prototype.exitScope = function() { | |
190 var start = this.scopeIndices.pop(); | |
191 for (var i = start; i < this.handleStack.length; ++i) { | |
192 var key = this.handleStack[i]; | |
193 if (!this.globalIds.hasOwnProperty(key)) { | |
194 delete this.map[this.handleStack[i]]; | |
195 this._deletedCount++; | |
196 } | |
197 } | |
198 this.handleStack = this.handleStack.splice(0, start); | |
199 } | |
200 | |
201 // Makes this ID globally scope. It must be explicitly invalidated. | |
202 ProxiedObjectTable.prototype.globalize = function(id) { | |
203 this.globalIds[id] = true; | |
204 } | |
205 | |
206 // Invalidates this ID, potentially freeing its corresponding object. | |
207 ProxiedObjectTable.prototype.invalidate = function(id) { | |
208 var old = this.get(id); | |
209 delete this.globalIds[id]; | |
210 delete this.map[id]; | |
211 this._deletedCount++; | |
212 return old; | |
213 } | |
214 | |
215 // Gets the object or function corresponding to this ID. | |
216 ProxiedObjectTable.prototype.get = function (id) { | |
217 if (!this.map.hasOwnProperty(id)) { | |
218 throw 'Proxy ' + id + ' has been invalidated.' | |
219 } | |
220 return this.map[id]; | |
221 } | |
222 | |
223 ProxiedObjectTable.prototype._initialize = function () { | |
224 // Configure this table's port to forward methods, getters, and setters | |
225 // from the remote proxy to the local object. | |
226 var table = this; | |
227 | |
228 this.port.receive(function (message) { | |
229 // TODO(vsm): Support a mechanism to register a handler here. | |
230 try { | |
231 var object = table.get(message[0]); | |
232 var receiver = unbind(object); | |
233 var member = message[1]; | |
234 var kind = message[2]; | |
235 var args = message[3].map(deserialize); | |
236 if (kind == 'get') { | |
237 // Getter. | |
238 var field = member; | |
239 if (field in receiver && args.length == 0) { | |
240 var result = bindIfFunction(receiver[field], receiver); | |
241 return [ 'return', serialize(result) ]; | |
242 } | |
243 } else if (kind == 'set') { | |
244 // Setter. | |
245 var field = member; | |
246 if (args.length == 1) { | |
247 return [ 'return', serialize(receiver[field] = args[0]) ]; | |
248 } | |
249 } else if (kind == 'hasProperty') { | |
250 var field = member; | |
251 return [ 'return', field in receiver ]; | |
252 } else if (kind == 'apply') { | |
253 // Direct function invocation. | |
254 var _this = getBoundThis(object); | |
255 return [ 'return', serialize(receiver.apply(_this, args)) ]; | |
256 } else if (member == '[]' && args.length == 1) { | |
257 // Index getter. | |
258 var result = bindIfFunction(receiver[args[0]], receiver); | |
259 return [ 'return', serialize(result) ]; | |
260 } else if (member == '[]=' && args.length == 2) { | |
261 // Index setter. | |
262 return [ 'return', serialize(receiver[args[0]] = args[1]) ]; | |
263 } else { | |
264 // Member function invocation. | |
265 var f = receiver[member]; | |
266 if (f) { | |
267 var result = f.apply(receiver, args); | |
268 return [ 'return', serialize(result) ]; | |
269 } | |
270 } | |
271 return [ 'none' ]; | |
272 } catch (e) { | |
273 return [ 'throws', e.toString() ]; | |
274 } | |
275 }); | |
276 } | |
277 | |
278 // Singleton for local proxied objects. | |
279 var proxiedObjectTable = new ProxiedObjectTable(); | |
280 | |
281 // DOM element serialization code. | |
282 var _localNextElementId = 0; | |
283 var _DART_ID = 'data-dart_id'; | |
284 var _DART_TEMPORARY_ATTACHED = 'data-dart_temporary_attached'; | |
285 | |
286 function serializeElement(e) { | |
287 // TODO(vsm): Use an isolate-specific id. | |
288 var id; | |
289 if (e.hasAttribute(_DART_ID)) { | |
290 id = e.getAttribute(_DART_ID); | |
291 } else { | |
292 id = (_localNextElementId++).toString(); | |
293 e.setAttribute(_DART_ID, id); | |
294 } | |
295 if (e !== document.documentElement) { | |
296 // Element must be attached to DOM to be retrieve in js part. | |
297 // Attach top unattached parent to avoid detaching parent of "e" when | |
298 // appending "e" directly to document. We keep count of elements | |
299 // temporarily attached to prevent detaching top unattached parent to | |
300 // early. This count is equals to the length of _DART_TEMPORARY_ATTACHED | |
301 // attribute. There could be other elements to serialize having the same | |
302 // top unattached parent. | |
303 var top = e; | |
304 while (true) { | |
305 if (top.hasAttribute(_DART_TEMPORARY_ATTACHED)) { | |
306 var oldValue = top.getAttribute(_DART_TEMPORARY_ATTACHED); | |
307 var newValue = oldValue + "a"; | |
308 top.setAttribute(_DART_TEMPORARY_ATTACHED, newValue); | |
309 break; | |
310 } | |
311 if (top.parentNode == null) { | |
312 top.setAttribute(_DART_TEMPORARY_ATTACHED, "a"); | |
313 document.documentElement.appendChild(top); | |
314 break; | |
315 } | |
316 if (top.parentNode === document.documentElement) { | |
317 // e was already attached to dom | |
318 break; | |
319 } | |
320 top = top.parentNode; | |
321 } | |
322 } | |
323 return id; | |
324 } | |
325 | |
326 function deserializeElement(id) { | |
327 // TODO(vsm): Clear the attribute. | |
328 var list = document.querySelectorAll('[' + _DART_ID + '="' + id + '"]'); | |
329 | |
330 if (list.length > 1) throw 'Non unique ID: ' + id; | |
331 if (list.length == 0) { | |
332 throw 'Element must be attached to the document: ' + id; | |
333 } | |
334 var e = list[0]; | |
335 if (e !== document.documentElement) { | |
336 // detach temporary attached element | |
337 var top = e; | |
338 while (true) { | |
339 if (top.hasAttribute(_DART_TEMPORARY_ATTACHED)) { | |
340 var oldValue = top.getAttribute(_DART_TEMPORARY_ATTACHED); | |
341 var newValue = oldValue.substring(1); | |
342 top.setAttribute(_DART_TEMPORARY_ATTACHED, newValue); | |
343 // detach top only if no more elements have to be unserialized | |
344 if (top.getAttribute(_DART_TEMPORARY_ATTACHED).length === 0) { | |
345 top.removeAttribute(_DART_TEMPORARY_ATTACHED); | |
346 document.documentElement.removeChild(top); | |
347 } | |
348 break; | |
349 } | |
350 if (top.parentNode === document.documentElement) { | |
351 // e was already attached to dom | |
352 break; | |
353 } | |
354 top = top.parentNode; | |
355 } | |
356 } | |
357 return e; | |
358 } | |
359 | |
360 | |
361 // Type for remote proxies to Dart objects. | |
362 function DartProxy(id, sendPort) { | |
363 this.id = id; | |
364 this.port = sendPort; | |
365 } | |
366 | |
367 // Serializes JS types to SendPortSync format: | |
368 // - primitives -> primitives | |
369 // - sendport -> sendport | |
370 // - DOM element -> [ 'domref', element-id ] | |
371 // - Function -> [ 'funcref', function-id, sendport ] | |
372 // - Object -> [ 'objref', object-id, sendport ] | |
373 function serialize(message) { | |
374 if (message == null) { | |
375 return null; // Convert undefined to null. | |
376 } else if (typeof(message) == 'string' || | |
377 typeof(message) == 'number' || | |
378 typeof(message) == 'boolean') { | |
379 // Primitives are passed directly through. | |
380 return message; | |
381 } else if (message instanceof SendPortSync) { | |
382 // Non-proxied objects are serialized. | |
383 return message; | |
384 } else if (message instanceof Element && | |
385 (message.ownerDocument == null || message.ownerDocument == document)) { | |
386 return [ 'domref', serializeElement(message) ]; | |
387 } else if (message instanceof BoundFunction && | |
388 typeof(message.object) == 'function') { | |
389 // Local function proxy. | |
390 return [ 'funcref', | |
391 proxiedObjectTable.add(message), | |
392 proxiedObjectTable.sendPort ]; | |
393 } else if (typeof(message) == 'function') { | |
394 if ('_dart_id' in message) { | |
395 // Remote function proxy. | |
396 var remoteId = message._dart_id; | |
397 var remoteSendPort = message._dart_port; | |
398 return [ 'funcref', remoteId, remoteSendPort ]; | |
399 } else { | |
400 // Local function proxy. | |
401 return [ 'funcref', | |
402 proxiedObjectTable.add(message), | |
403 proxiedObjectTable.sendPort ]; | |
404 } | |
405 } else if (message instanceof DartProxy) { | |
406 // Remote object proxy. | |
407 return [ 'objref', message.id, message.port ]; | |
408 } else { | |
409 // Local object proxy. | |
410 return [ 'objref', | |
411 proxiedObjectTable.add(message), | |
412 proxiedObjectTable.sendPort ]; | |
413 } | |
414 } | |
415 | |
416 function deserialize(message) { | |
417 if (message == null) { | |
418 return null; // Convert undefined to null. | |
419 } else if (typeof(message) == 'string' || | |
420 typeof(message) == 'number' || | |
421 typeof(message) == 'boolean') { | |
422 // Primitives are passed directly through. | |
423 return message; | |
424 } else if (message instanceof SendPortSync) { | |
425 // Serialized type. | |
426 return message; | |
427 } | |
428 var tag = message[0]; | |
429 switch (tag) { | |
430 case 'funcref': return deserializeFunction(message); | |
431 case 'objref': return deserializeObject(message); | |
432 case 'domref': return deserializeElement(message[1]); | |
433 } | |
434 throw 'Unsupported serialized data: ' + message; | |
435 } | |
436 | |
437 // Create a local function that forwards to the remote function. | |
438 function deserializeFunction(message) { | |
439 var id = message[1]; | |
440 var port = message[2]; | |
441 // TODO(vsm): Add a more robust check for a local SendPortSync. | |
442 if ("receivePort" in port) { | |
443 // Local function. | |
444 return unbind(proxiedObjectTable.get(id)); | |
445 } else { | |
446 // Remote function. Forward to its port. | |
447 var f = function () { | |
448 var depth = enterScope(); | |
449 try { | |
450 var args = Array.prototype.slice.apply(arguments); | |
451 args.splice(0, 0, this); | |
452 args = args.map(serialize); | |
453 var result = port.callSync([id, '#call', args]); | |
454 if (result[0] == 'throws') throw deserialize(result[1]); | |
455 return deserialize(result[1]); | |
456 } finally { | |
457 exitScope(depth); | |
458 } | |
459 }; | |
460 // Cache the remote id and port. | |
461 f._dart_id = id; | |
462 f._dart_port = port; | |
463 return f; | |
464 } | |
465 } | |
466 | |
467 // Creates a DartProxy to forwards to the remote object. | |
468 function deserializeObject(message) { | |
469 var id = message[1]; | |
470 var port = message[2]; | |
471 // TODO(vsm): Add a more robust check for a local SendPortSync. | |
472 if ("receivePort" in port) { | |
473 // Local object. | |
474 return proxiedObjectTable.get(id); | |
475 } else { | |
476 // Remote object. | |
477 return new DartProxy(id, port); | |
478 } | |
479 } | |
480 | |
481 // Remote handler to construct a new JavaScript object given its | |
482 // serialized constructor and arguments. | |
483 function construct(args) { | |
484 args = args.map(deserialize); | |
485 var constructor = unbind(args[0]); | |
486 args = Array.prototype.slice.call(args, 1); | |
487 | |
488 // Until 10 args, the 'new' operator is used. With more arguments we use a | |
489 // generic way that may not work, particulary when the constructor does not | |
vsm
2013/05/29 04:55:39
particulary -> particularly
alexandre.ardhuin
2013/05/31 19:11:13
Done.
| |
490 // have an "apply" method. | |
491 var ret = null; | |
492 if (args.length === 0) { | |
493 ret = new constructor(); | |
494 } else if (args.length === 1) { | |
495 ret = new constructor(args[0]); | |
496 } else if (args.length === 2) { | |
497 ret = new constructor(args[0], args[1]); | |
498 } else if (args.length === 3) { | |
499 ret = new constructor(args[0], args[1], args[2]); | |
500 } else if (args.length === 4) { | |
501 ret = new constructor(args[0], args[1], args[2], args[3]); | |
502 } else if (args.length === 5) { | |
503 ret = new constructor(args[0], args[1], args[2], args[3], args[4]); | |
504 } else if (args.length === 6) { | |
505 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
506 args[5]); | |
507 } else if (args.length === 7) { | |
508 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
509 args[5], args[6]); | |
510 } else if (args.length === 8) { | |
511 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
512 args[5], args[6], args[7]); | |
513 } else if (args.length === 9) { | |
514 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
515 args[5], args[6], args[7], args[8]); | |
516 } else if (args.length === 10) { | |
517 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
518 args[5], args[6], args[7], args[8], args[9]); | |
519 } else { | |
520 // Dummy Type with correct constructor. | |
521 var Type = function(){}; | |
522 Type.prototype = constructor.prototype; | |
523 | |
524 // Create a new instance | |
525 var instance = new Type(); | |
526 | |
527 // Call the original constructor. | |
528 ret = constructor.apply(instance, args); | |
529 ret = Object(ret) === ret ? ret : instance; | |
530 } | |
531 return serialize(ret); | |
532 } | |
533 | |
534 // Remote handler to return the top-level JavaScript context. | |
535 function context(data) { | |
536 return serialize(globalContext); | |
537 } | |
538 | |
539 // Remote handler to track number of live / allocated proxies. | |
540 function proxyCount() { | |
541 var live = proxiedObjectTable.count(); | |
542 var total = proxiedObjectTable.total(); | |
543 return [live, total]; | |
544 } | |
545 | |
546 // Return true if two JavaScript proxies are equal (==). | |
547 function proxyEquals(args) { | |
548 return deserialize(args[0]) == deserialize(args[1]); | |
549 } | |
550 | |
551 // Return true if a JavaScript proxy is instance of a given type (instanceof). | |
552 function proxyInstanceof(args) { | |
553 var obj = unbind(deserialize(args[0])); | |
554 var type = unbind(deserialize(args[1])); | |
555 return obj instanceof type; | |
556 } | |
557 | |
558 // Return true if a JavaScript proxy is instance of a given type (instanceof). | |
559 function proxyDeleteProperty(args) { | |
560 var obj = unbind(deserialize(args[0])); | |
561 var member = unbind(deserialize(args[1])); | |
562 delete obj[member]; | |
563 } | |
564 | |
565 function proxyConvert(args) { | |
566 return serialize(deserializeDataTree(args)); | |
567 } | |
568 | |
569 function deserializeDataTree(data) { | |
570 var type = data[0]; | |
571 var value = data[1]; | |
572 if (type === 'map') { | |
573 var obj = {}; | |
574 for (var i = 0; i < value.length; i++) { | |
575 obj[value[i][0]] = deserializeDataTree(value[i][1]); | |
576 } | |
577 return obj; | |
578 } else if (type === 'list') { | |
579 var list = []; | |
580 for (var i = 0; i < value.length; i++) { | |
581 list.push(deserializeDataTree(value[i])); | |
582 } | |
583 return list; | |
584 } else /* 'simple' */ { | |
585 return deserialize(value); | |
586 } | |
587 } | |
588 | |
589 function makeGlobalPort(name, f) { | |
590 var port = new ReceivePortSync(); | |
591 port.receive(f); | |
592 window.registerPort(name, port.toSendPort()); | |
593 } | |
594 | |
595 // Enters a new scope in the JavaScript context. | |
596 function enterJavaScriptScope() { | |
597 proxiedObjectTable.enterScope(); | |
598 } | |
599 | |
600 // Enters a new scope in both the JavaScript and Dart context. | |
601 var _dartEnterScopePort = null; | |
602 function enterScope() { | |
603 enterJavaScriptScope(); | |
604 if (!_dartEnterScopePort) { | |
605 _dartEnterScopePort = window.lookupPort('js-dart-enter-scope'); | |
606 } | |
607 return _dartEnterScopePort.callSync([]); | |
608 } | |
609 | |
610 // Exits the current scope (and invalidate local IDs) in the JavaScript | |
611 // context. | |
612 function exitJavaScriptScope() { | |
613 proxiedObjectTable.exitScope(); | |
614 } | |
615 | |
616 // Exits the current scope in both the JavaScript and Dart context. | |
617 var _dartExitScopePort = null; | |
618 function exitScope(depth) { | |
619 exitJavaScriptScope(); | |
620 if (!_dartExitScopePort) { | |
621 _dartExitScopePort = window.lookupPort('js-dart-exit-scope'); | |
622 } | |
623 return _dartExitScopePort.callSync([ depth ]); | |
624 } | |
625 | |
626 makeGlobalPort('dart-js-context', context); | |
627 makeGlobalPort('dart-js-create', construct); | |
628 makeGlobalPort('dart-js-proxy-count', proxyCount); | |
629 makeGlobalPort('dart-js-equals', proxyEquals); | |
630 makeGlobalPort('dart-js-instanceof', proxyInstanceof); | |
631 makeGlobalPort('dart-js-delete-property', proxyDeleteProperty); | |
632 makeGlobalPort('dart-js-convert', proxyConvert); | |
633 makeGlobalPort('dart-js-enter-scope', enterJavaScriptScope); | |
634 makeGlobalPort('dart-js-exit-scope', exitJavaScriptScope); | |
635 makeGlobalPort('dart-js-globalize', function(data) { | |
636 if (data[0] == "objref") return proxiedObjectTable.globalize(data[1]); | |
637 // TODO(vsm): Do we ever need to globalize functions? | |
638 throw 'Illegal type: ' + data[0]; | |
639 }); | |
640 makeGlobalPort('dart-js-invalidate', function(data) { | |
641 if (data[0] == "objref") return proxiedObjectTable.invalidate(data[1]); | |
642 // TODO(vsm): Do we ever need to globalize functions? | |
643 throw 'Illegal type: ' + data[0]; | |
644 }); | |
645 })(); | |
646 """; | |
647 | |
648 // Injects JavaScript source code onto the page. | |
649 // This is only used to load the bootstrapping code above. | |
650 void _inject(code) { | |
651 final script = new ScriptElement(); | |
652 script.type = 'text/javascript'; | |
653 script.innerHtml = code; | |
654 document.body.nodes.add(script); | |
655 } | |
656 | |
657 // Global ports to manage communication from Dart to JS. | |
658 SendPortSync _jsPortSync = null; | |
659 SendPortSync _jsPortCreate = null; | |
660 SendPortSync _jsPortProxyCount = null; | |
661 SendPortSync _jsPortEquals = null; | |
662 SendPortSync _jsPortInstanceof = null; | |
663 SendPortSync _jsPortDeleteProperty = null; | |
664 SendPortSync _jsPortConvert = null; | |
665 SendPortSync _jsEnterJavaScriptScope = null; | |
666 SendPortSync _jsExitJavaScriptScope = null; | |
667 SendPortSync _jsGlobalize = null; | |
668 SendPortSync _jsInvalidate = null; | |
669 | |
670 // Global ports to manage communication from JS to Dart. | |
671 ReceivePortSync _dartEnterDartScope = null; | |
672 ReceivePortSync _dartExitDartScope = null; | |
673 | |
674 // Initializes bootstrap code and ports. | |
675 void _initialize() { | |
676 if (_jsPortSync != null) return; | |
677 | |
678 // Test if the port is already defined. | |
679 try { | |
680 _jsPortSync = window.lookupPort('dart-js-context'); | |
681 } catch (e) { | |
682 // TODO(vsm): Suppress the exception until dartbug.com/5854 is fixed. | |
683 } | |
684 | |
685 // If not, try injecting the script. | |
686 if (_jsPortSync == null) { | |
687 _inject(_JS_BOOTSTRAP); | |
688 _jsPortSync = window.lookupPort('dart-js-context'); | |
689 } | |
690 | |
691 _jsPortCreate = window.lookupPort('dart-js-create'); | |
692 _jsPortProxyCount = window.lookupPort('dart-js-proxy-count'); | |
693 _jsPortEquals = window.lookupPort('dart-js-equals'); | |
694 _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); | |
695 _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property'); | |
696 _jsPortConvert = window.lookupPort('dart-js-convert'); | |
697 _jsEnterJavaScriptScope = window.lookupPort('dart-js-enter-scope'); | |
698 _jsExitJavaScriptScope = window.lookupPort('dart-js-exit-scope'); | |
699 _jsGlobalize = window.lookupPort('dart-js-globalize'); | |
700 _jsInvalidate = window.lookupPort('dart-js-invalidate'); | |
701 | |
702 _dartEnterDartScope = new ReceivePortSync() | |
703 ..receive((_) => _enterScope()); | |
704 _dartExitDartScope = new ReceivePortSync() | |
705 ..receive((args) => _exitScope(args[0])); | |
706 window.registerPort('js-dart-enter-scope', _dartEnterDartScope.toSendPort()); | |
707 window.registerPort('js-dart-exit-scope', _dartExitDartScope.toSendPort()); | |
708 } | |
709 | |
710 /** | |
711 * Returns a proxy to the global JavaScript context for this page. | |
712 */ | |
713 JsObject get context { | |
714 _enterScopeIfNeeded(); | |
715 return _deserialize(_jsPortSync.callSync([])); | |
716 } | |
717 | |
718 // Depth of current scope. Return 0 if no scope. | |
719 get _depth => _proxiedObjectTable._scopeIndices.length; | |
720 | |
721 // If we are not already in a scope, enter one and register a | |
722 // corresponding exit once we return to the event loop. | |
723 void _enterScopeIfNeeded() { | |
724 if (_depth == 0) { | |
725 var depth = _enterScope(); | |
726 runAsync(() => _exitScope(depth)); | |
727 } | |
728 } | |
729 | |
730 /** | |
731 * Executes the closure [f] within a scope. Any proxies created within this | |
732 * scope are invalidated afterward unless they are converted to a global proxy. | |
733 */ | |
734 scoped(f) { | |
735 var depth = _enterScope(); | |
736 try { | |
737 return f(); | |
738 } finally { | |
739 _exitScope(depth); | |
740 } | |
741 } | |
742 | |
743 int _enterScope() { | |
744 _initialize(); | |
745 _proxiedObjectTable.enterScope(); | |
746 _jsEnterJavaScriptScope.callSync([]); | |
747 return _proxiedObjectTable._scopeIndices.length; | |
748 } | |
749 | |
750 void _exitScope(int depth) { | |
751 assert(_proxiedObjectTable._scopeIndices.length == depth); | |
752 _jsExitJavaScriptScope.callSync([]); | |
753 _proxiedObjectTable.exitScope(); | |
754 } | |
755 | |
756 /* | |
757 * Enters a scope and returns the depth of the scope stack. | |
758 */ | |
759 /// WARNING: This API is experimental and may be removed. | |
vsm
2013/05/29 04:55:39
We can get rid of this.
alexandre.ardhuin
2013/05/31 19:11:13
Done.
| |
760 int $experimentalEnterScope() { | |
761 return _enterScope(); | |
762 } | |
763 | |
764 /* | |
765 * Exits a scope. The [depth] must match that returned by the corresponding | |
766 * enter scope call. | |
767 */ | |
768 /// WARNING: This API is experimental and may be removed. | |
vsm
2013/05/29 04:55:39
This too.
alexandre.ardhuin
2013/05/31 19:11:13
Done.
| |
769 void $experimentalExitScope(int depth) { | |
770 _exitScope(depth); | |
771 } | |
772 | |
773 /** | |
774 * Retains the given [object] beyond the current scope. | |
775 * Instead, it will need to be explicitly released. | |
776 * The given [object] is returned for convenience. | |
777 */ | |
778 // TODO(aa) : change dynamic to Serializable<Proxy> if http://dartbug.com/9023 | |
779 // is fixed. | |
780 // TODO(aa) : change to "<T extends Serializable<Proxy>> T retain(T object)" | |
781 // once generic methods have landed. | |
782 dynamic retain(Serializable<JsObject> object) { | |
783 _jsGlobalize.callSync(_serialize(object.toJs())); | |
784 return object; | |
785 } | |
786 | |
787 /** | |
788 * Releases a retained [object]. | |
789 */ | |
790 void release(Serializable<JsObject> object) { | |
791 _jsInvalidate.callSync(_serialize(object.toJs())); | |
792 } | |
793 | |
794 /** | |
795 * Converts a json-like [data] to a JavaScript map and return a [JsObject] to it . | |
vsm
2013/05/29 04:55:39
JavaScript map or array
line length just over 80
alexandre.ardhuin
2013/05/31 19:11:13
Done.
| |
796 */ | |
797 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); | |
798 | |
799 /** | |
800 * Converts a local Dart function to a callback that can be passed to | |
801 * JavaScript. | |
802 * | |
803 * A callback can either be: | |
804 * | |
805 * - single-fire, in which case it is automatically invalidated after the first | |
806 * invocation, or | |
807 * - multi-fire, in which case it must be explicitly disposed. | |
808 */ | |
809 class Callback implements Serializable<JsFunction> { | |
810 final bool _manualDispose; | |
811 JsFunction _f; | |
812 | |
813 Callback._internal(this._manualDispose, Function f, bool withThis) { | |
814 final id = _proxiedObjectTable.add((thisArg, List args) { | |
815 final arguments = new List.from(args); | |
816 if (withThis) arguments.insert(0, thisArg); | |
817 if (_manualDispose) { | |
818 try { | |
819 return Function.apply(f, arguments); | |
820 } finally { | |
821 _dispose(); | |
822 } | |
823 } else { | |
824 return Function.apply(f, arguments); | |
825 } | |
826 }); | |
827 _proxiedObjectTable.globalize(id); | |
828 _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id); | |
829 } | |
830 | |
831 _dispose() { | |
832 _proxiedObjectTable.invalidate(_f._id); | |
833 } | |
834 | |
835 JsFunction toJs() => _f; | |
836 | |
837 /** | |
838 * Disposes this [Callback] so that it may be collected. | |
839 * Once a [Callback] is disposed, it is an error to invoke it from JavaScript. | |
840 */ | |
841 dispose() { | |
842 assert(_manualDispose); | |
843 _dispose(); | |
844 } | |
845 | |
846 /** | |
847 * Creates a single-fire [Callback] that invokes [f]. The callback is | |
848 * automatically disposed after the first invocation. | |
849 */ | |
850 factory Callback.once(Function f, {bool withThis: false}) => | |
851 new Callback._internal(false, f, withThis); | |
852 | |
853 /** | |
854 * Creates a multi-fire [Callback] that invokes [f]. The callback must be | |
855 * explicitly disposed to avoid memory leaks. | |
856 */ | |
857 factory Callback.many(Function f, {bool withThis: false}) => | |
858 new Callback._internal(true, f, withThis); | |
859 } | |
860 | |
861 /** | |
862 * Proxies to JavaScript objects. | |
863 */ | |
864 class JsObject implements Serializable<JsObject> { | |
865 final SendPortSync _port; | |
866 final String _id; | |
867 | |
868 /** | |
869 * Constructs a [JsObject] to a new JavaScript object by invoking a (proxy to | |
870 * a) JavaScript [constructor]. The [arguments] list should contain either | |
871 * primitive values, DOM elements, or Proxies. | |
872 */ | |
873 factory JsObject(Serializable<JsFunction> constructor, [List arguments]) { | |
874 _enterScopeIfNeeded(); | |
875 final params = [constructor]; | |
876 if (arguments != null) params.addAll(arguments); | |
877 final serialized = params.map(_serialize).toList(); | |
878 final result = _jsPortCreate.callSync(serialized); | |
879 return _deserialize(result); | |
880 } | |
881 | |
882 /** | |
883 * Constructs a [JsObject] to a new JavaScript map or list created defined via | |
884 * Dart map or list. | |
885 */ | |
886 factory JsObject._json(data) { | |
887 _enterScopeIfNeeded(); | |
888 return _convert(data); | |
889 } | |
890 | |
891 static _convert(data) { | |
892 return _deserialize(_jsPortConvert.callSync(_serializeDataTree(data))); | |
893 } | |
894 | |
895 static _serializeDataTree(data) { | |
896 if (data is Map) { | |
897 final entries = new List(); | |
898 for (var key in data.keys) { | |
899 entries.add([key, _serializeDataTree(data[key])]); | |
900 } | |
901 return ['map', entries]; | |
902 } else if (data is Iterable) { | |
903 return ['list', data.map(_serializeDataTree).toList()]; | |
904 } else { | |
905 return ['simple', _serialize(data)]; | |
906 } | |
907 } | |
908 | |
909 JsObject._internal(this._port, this._id); | |
910 | |
911 JsObject toJs() => this; | |
912 | |
913 // Resolve whether this is needed. | |
914 operator[](arg) => _forward(this, '[]', 'method', [ arg ]); | |
915 | |
916 // Resolve whether this is needed. | |
917 operator[]=(key, value) => _forward(this, '[]=', 'method', [ key, value ]); | |
918 | |
919 // Test if this is equivalent to another Proxy. This essentially | |
920 // maps to JavaScript's == operator. | |
921 // TODO(vsm): Can we avoid forwarding to JS? | |
922 operator==(other) => identical(this, other) | |
923 ? true | |
924 : (other is JsObject && | |
925 _jsPortEquals.callSync([_serialize(this), _serialize(other)])); | |
926 | |
927 bool hasProperty(String property) => | |
vsm
2013/05/29 04:55:39
Needs a comment.
Should we make these top-level i
alexandre.ardhuin
2013/05/31 19:11:13
Commented.
Perhaps we could avoid NSM here and ke
| |
928 _forward(this, property, 'hasProperty', []); | |
929 | |
930 /** | |
931 * Delete the [name] property. | |
932 */ | |
933 void deleteProperty(String name) { | |
934 _jsPortDeleteProperty.callSync([this, name].map(_serialize).toList()); | |
935 } | |
936 | |
937 /** | |
938 * Check if this is instance of [type]. | |
939 */ | |
940 bool instanceof(Serializable<JsFunction> type) => | |
941 _jsPortInstanceof.callSync([this, type].map(_serialize).toList()); | |
942 | |
943 String toString() { | |
944 try { | |
945 return _forward(this, 'toString', 'method', []); | |
946 } catch(e) { | |
947 return super.toString(); | |
948 } | |
949 } | |
950 | |
951 callMethod(String name, List arguments) { | |
952 return _forward(this, name, 'method', arguments); | |
953 } | |
954 | |
955 // Forward member accesses to the backing JavaScript object. | |
956 static _forward(JsObject receiver, String member, String kind, List args) { | |
957 _enterScopeIfNeeded(); | |
958 var result = receiver._port.callSync([receiver._id, member, kind, | |
959 args.map(_serialize).toList()]); | |
960 switch (result[0]) { | |
961 case 'return': return _deserialize(result[1]); | |
962 case 'throws': throw _deserialize(result[1]); | |
963 case 'none': throw new NoSuchMethodError(receiver, member, args, {}); | |
964 default: throw 'Invalid return value'; | |
965 } | |
966 } | |
967 } | |
968 | |
969 /// A [JsObject] subtype to JavaScript functions. | |
970 class JsFunction extends JsObject implements Serializable<JsFunction> { | |
971 JsFunction._internal(SendPortSync port, String id) : super._internal(port, id) ; | |
972 | |
973 apply(thisArg, [List args]) { | |
974 return JsObject._forward(this, '', 'apply', [thisArg, args == null ? null : args.map(_serialize).toList()]); | |
975 } | |
976 } | |
977 | |
978 /// Marker class used to indicate it is serializable to js. If a class is a | |
979 /// [Serializable] the "toJs" method will be called and the result will be used | |
980 /// as value. | |
981 abstract class Serializable<T> { | |
982 T toJs(); | |
983 } | |
984 | |
985 // A table to managed local Dart objects that are proxied in JavaScript. | |
986 class _ProxiedObjectTable { | |
987 // Debugging name. | |
988 final String _name; | |
989 | |
990 // Generator for unique IDs. | |
991 int _nextId; | |
992 | |
993 // Counter for invalidated IDs for debugging. | |
994 int _deletedCount; | |
995 | |
996 // Table of IDs to Dart objects. | |
997 final Map<String, Object> _registry; | |
998 | |
999 // Port to handle and forward requests to the underlying Dart objects. | |
1000 // A remote proxy is uniquely identified by an ID and SendPortSync. | |
1001 final ReceivePortSync _port; | |
1002 | |
1003 // The set of IDs that are global. These must be explicitly invalidated. | |
1004 final Set<String> _globalIds; | |
1005 | |
1006 // The stack of valid IDs. | |
1007 final List<String> _handleStack; | |
1008 | |
1009 // The stack of scopes, where each scope is represented by an index into the | |
1010 // handleStack. | |
1011 final List<int> _scopeIndices; | |
1012 | |
1013 // Enters a new scope. | |
1014 enterScope() { | |
1015 _scopeIndices.add(_handleStack.length); | |
1016 } | |
1017 | |
1018 // Invalidates non-global IDs created in the current scope and | |
1019 // restore to the previous scope. | |
1020 exitScope() { | |
1021 int start = _scopeIndices.removeLast(); | |
1022 for (int i = start; i < _handleStack.length; ++i) { | |
1023 String key = _handleStack[i]; | |
1024 if (!_globalIds.contains(key)) { | |
1025 _registry.remove(_handleStack[i]); | |
1026 _deletedCount++; | |
1027 } | |
1028 } | |
1029 if (start != _handleStack.length) { | |
1030 _handleStack.removeRange(start, _handleStack.length - start); | |
1031 } | |
1032 } | |
1033 | |
1034 // Converts an ID to a global. | |
1035 globalize(id) => _globalIds.add(id); | |
1036 | |
1037 // Invalidates an ID. | |
1038 invalidate(id) { | |
1039 var old = _registry[id]; | |
1040 _globalIds.remove(id); | |
1041 _registry.remove(id); | |
1042 _deletedCount++; | |
1043 return old; | |
1044 } | |
1045 | |
1046 // Replaces the object referenced by an ID. | |
1047 _replace(id, x) { | |
1048 _registry[id] = x; | |
1049 } | |
1050 | |
1051 _ProxiedObjectTable() : | |
1052 _name = 'dart-ref', | |
1053 _nextId = 0, | |
1054 _deletedCount = 0, | |
1055 _registry = {}, | |
1056 _port = new ReceivePortSync(), | |
1057 _handleStack = new List<String>(), | |
1058 _scopeIndices = new List<int>(), | |
1059 _globalIds = new Set<String>() { | |
1060 _port.receive((msg) { | |
1061 try { | |
1062 final receiver = _registry[msg[0]]; | |
1063 final method = msg[1]; | |
1064 final args = msg[2].map(_deserialize).toList(); | |
1065 if (method == '#call') { | |
1066 final func = receiver as Function; | |
1067 var result = _serialize(func(args)); | |
1068 return ['return', result]; | |
1069 } else { | |
1070 // TODO(vsm): Support a mechanism to register a handler here. | |
1071 throw 'Invocation unsupported on non-function Dart proxies'; | |
1072 } | |
1073 } catch (e) { | |
1074 // TODO(vsm): callSync should just handle exceptions itself. | |
1075 return ['throws', '$e']; | |
1076 } | |
1077 }); | |
1078 } | |
1079 | |
1080 // Adds a new object to the table and return a new ID for it. | |
1081 String add(x) { | |
1082 _enterScopeIfNeeded(); | |
1083 // TODO(vsm): Cache x and reuse id. | |
1084 final id = '$_name-${_nextId++}'; | |
1085 _registry[id] = x; | |
1086 _handleStack.add(id); | |
1087 return id; | |
1088 } | |
1089 | |
1090 // Gets an object by ID. | |
1091 Object get(String id) { | |
1092 return _registry[id]; | |
1093 } | |
1094 | |
1095 // Gets the current number of objects kept alive by this table. | |
1096 get count => _registry.length; | |
1097 | |
1098 // Gets the total number of IDs ever allocated. | |
1099 get total => count + _deletedCount; | |
1100 | |
1101 // Gets a send port for this table. | |
1102 get sendPort => _port.toSendPort(); | |
1103 } | |
1104 | |
1105 // The singleton to manage proxied Dart objects. | |
1106 _ProxiedObjectTable _proxiedObjectTable = new _ProxiedObjectTable(); | |
1107 | |
1108 /// End of proxy implementation. | |
1109 | |
1110 // Dart serialization support. | |
1111 | |
1112 _serialize(var message) { | |
1113 if (message == null) { | |
1114 return null; // Convert undefined to null. | |
1115 } else if (message is String || | |
1116 message is num || | |
1117 message is bool) { | |
1118 // Primitives are passed directly through. | |
1119 return message; | |
1120 } else if (message is SendPortSync) { | |
1121 // Non-proxied objects are serialized. | |
1122 return message; | |
1123 } else if (message is Element && | |
1124 (message.document == null || message.document == document)) { | |
1125 return [ 'domref', _serializeElement(message) ]; | |
1126 } else if (message is JsFunction) { | |
1127 // Remote function proxy. | |
1128 return [ 'funcref', message._id, message._port ]; | |
1129 } else if (message is JsObject) { | |
1130 // Remote object proxy. | |
1131 return [ 'objref', message._id, message._port ]; | |
1132 } else if (message is Serializable) { | |
1133 // use of result of toJs() | |
1134 return _serialize(message.toJs()); | |
1135 } else { | |
1136 // Local object proxy. | |
1137 return [ 'objref', | |
1138 _proxiedObjectTable.add(message), | |
1139 _proxiedObjectTable.sendPort ]; | |
1140 } | |
1141 } | |
1142 | |
1143 _deserialize(var message) { | |
1144 deserializeFunction(message) { | |
1145 var id = message[1]; | |
1146 var port = message[2]; | |
1147 if (port == _proxiedObjectTable.sendPort) { | |
1148 // Local function. | |
1149 return _proxiedObjectTable.get(id); | |
1150 } else { | |
1151 // Remote function. Forward to its port. | |
1152 return new JsFunction._internal(port, id); | |
1153 } | |
1154 } | |
1155 | |
1156 deserializeObject(message) { | |
1157 var id = message[1]; | |
1158 var port = message[2]; | |
1159 if (port == _proxiedObjectTable.sendPort) { | |
1160 // Local object. | |
1161 return _proxiedObjectTable.get(id); | |
1162 } else { | |
1163 // Remote object. | |
1164 return new JsObject._internal(port, id); | |
1165 } | |
1166 } | |
1167 | |
1168 | |
1169 if (message == null) { | |
1170 return null; // Convert undefined to null. | |
1171 } else if (message is String || | |
1172 message is num || | |
1173 message is bool) { | |
1174 // Primitives are passed directly through. | |
1175 return message; | |
1176 } else if (message is SendPortSync) { | |
1177 // Serialized type. | |
1178 return message; | |
1179 } | |
1180 var tag = message[0]; | |
1181 switch (tag) { | |
1182 case 'funcref': return deserializeFunction(message); | |
1183 case 'objref': return deserializeObject(message); | |
1184 case 'domref': return _deserializeElement(message[1]); | |
1185 } | |
1186 throw 'Unsupported serialized data: $message'; | |
1187 } | |
1188 | |
1189 // DOM element serialization. | |
1190 | |
1191 int _localNextElementId = 0; | |
1192 | |
1193 const _DART_ID = 'data-dart_id'; | |
1194 const _DART_TEMPORARY_ATTACHED = 'data-dart_temporary_attached'; | |
1195 | |
1196 _serializeElement(Element e) { | |
1197 // TODO(vsm): Use an isolate-specific id. | |
1198 var id; | |
1199 if (e.attributes.containsKey(_DART_ID)) { | |
1200 id = e.attributes[_DART_ID]; | |
1201 } else { | |
1202 id = 'dart-${_localNextElementId++}'; | |
1203 e.attributes[_DART_ID] = id; | |
1204 } | |
1205 if (!identical(e, document.documentElement)) { | |
1206 // Element must be attached to DOM to be retrieve in js part. | |
1207 // Attach top unattached parent to avoid detaching parent of "e" when | |
1208 // appending "e" directly to document. We keep count of elements | |
1209 // temporarily attached to prevent detaching top unattached parent to | |
1210 // early. This count is equals to the length of _DART_TEMPORARY_ATTACHED | |
1211 // attribute. There could be other elements to serialize having the same | |
1212 // top unattached parent. | |
1213 var top = e; | |
1214 while (true) { | |
1215 if (top.attributes.containsKey(_DART_TEMPORARY_ATTACHED)) { | |
1216 final oldValue = top.attributes[_DART_TEMPORARY_ATTACHED]; | |
1217 final newValue = oldValue + 'a'; | |
1218 top.attributes[_DART_TEMPORARY_ATTACHED] = newValue; | |
1219 break; | |
1220 } | |
1221 if (top.parent == null) { | |
1222 top.attributes[_DART_TEMPORARY_ATTACHED] = 'a'; | |
1223 document.documentElement.children.add(top); | |
1224 break; | |
1225 } | |
1226 if (identical(top.parent, document.documentElement)) { | |
1227 // e was already attached to dom | |
1228 break; | |
1229 } | |
1230 top = top.parent; | |
1231 } | |
1232 } | |
1233 return id; | |
1234 } | |
1235 | |
1236 Element _deserializeElement(var id) { | |
1237 var list = queryAll('[$_DART_ID="$id"]'); | |
1238 if (list.length > 1) throw 'Non unique ID: $id'; | |
1239 if (list.length == 0) { | |
1240 throw 'Only elements attached to document can be serialized: $id'; | |
1241 } | |
1242 final e = list[0]; | |
1243 if (!identical(e, document.documentElement)) { | |
1244 // detach temporary attached element | |
1245 var top = e; | |
1246 while (true) { | |
1247 if (top.attributes.containsKey(_DART_TEMPORARY_ATTACHED)) { | |
1248 final oldValue = top.attributes[_DART_TEMPORARY_ATTACHED]; | |
1249 final newValue = oldValue.substring(1); | |
1250 top.attributes[_DART_TEMPORARY_ATTACHED] = newValue; | |
1251 // detach top only if no more elements have to be unserialized | |
1252 if (top.attributes[_DART_TEMPORARY_ATTACHED].length == 0) { | |
1253 top.attributes.remove(_DART_TEMPORARY_ATTACHED); | |
1254 top.remove(); | |
1255 } | |
1256 break; | |
1257 } | |
1258 if (identical(top.parent, document.documentElement)) { | |
1259 // e was already attached to dom | |
1260 break; | |
1261 } | |
1262 top = top.parent; | |
1263 } | |
1264 } | |
1265 return e; | |
1266 } | |
1267 | |
1268 // Fetch the number of proxies to JavaScript objects. | |
1269 // This returns a 2 element list. The first is the number of currently | |
1270 // live proxies. The second is the total number of proxies ever | |
1271 // allocated. | |
1272 List _proxyCountJavaScript() => _jsPortProxyCount.callSync([]); | |
1273 | |
1274 /** | |
1275 * Returns the number of allocated proxy objects matching the given | |
1276 * conditions. By default, the total number of live proxy objects are | |
1277 * return. In a well behaved program, this should stay below a small | |
1278 * bound. | |
1279 * | |
1280 * Set [all] to true to return the total number of proxies ever allocated. | |
1281 * Set [dartOnly] to only count proxies to Dart objects (live or all). | |
1282 * Set [jsOnly] to only count proxies to JavaScript objects (live or all). | |
1283 */ | |
1284 int proxyCount({all: false, dartOnly: false, jsOnly: false}) { | |
1285 final js = !dartOnly; | |
1286 final dart = !jsOnly; | |
1287 final jsCounts = js ? _proxyCountJavaScript() : null; | |
1288 var sum = 0; | |
1289 if (!all) { | |
1290 if (js) | |
1291 sum += jsCounts[0]; | |
1292 if (dart) | |
1293 sum += _proxiedObjectTable.count; | |
1294 } else { | |
1295 if (js) | |
1296 sum += jsCounts[1]; | |
1297 if (dart) | |
1298 sum += _proxiedObjectTable.total; | |
1299 } | |
1300 return sum; | |
1301 } | |
1302 | |
1303 // Prints the number of live handles in Dart and JavaScript. This is for | |
1304 // debugging / profiling purposes. | |
1305 void _proxyDebug([String message = '']) { | |
1306 print('Proxy status $message:'); | |
1307 var dartLive = proxyCount(dartOnly: true); | |
1308 var dartTotal = proxyCount(dartOnly: true, all: true); | |
1309 var jsLive = proxyCount(jsOnly: true); | |
1310 var jsTotal = proxyCount(jsOnly: true, all: true); | |
1311 print(' Dart objects Live : $dartLive (out of $dartTotal ever allocated).'); | |
1312 print(' JS objects Live : $jsLive (out of $jsTotal ever allocated).'); | |
1313 } | |
OLD | NEW |