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