Repository: zoofIO/flexx
Pull request #403 (2017-10-06 12:26:14)

Improve performance of event reconnecting
In one particular use-case, this significantly improves performance.
@ivoflipse @korijn


Patch:
diff --git a/docs/scripts/genexamples.py b/docs/scripts/genexamples.py
index c4ebff3..19b1178 100644
--- a/docs/scripts/genexamples.py
+++ b/docs/scripts/genexamples.py
@@ -6,7 +6,7 @@ import json
 from types import ModuleType
 import flexx
 from flexx import ui, app
-from urllib.request import urlopen
+from urllib.request import urlopen, Request
 
 from uiexample import create_ui_example
 
@@ -27,7 +27,9 @@ created_files = []
 def get_notebook_list():
     url = 'https://api.github.com/repos/zoofio/flexx-notebooks/contents'
     print('downloading %s ... ' % url, end='')
-    s = json.loads(urlopen(url, timeout=5.0).read().decode())
+    # https://github.com/travis-ci/travis-ci/issues/5649
+    req = Request(url, headers={'User-Agent': 'flexx/%s' % flexx.__version__})
+    s = json.loads(urlopen(req, timeout=5.0).read().decode())
     print('done')
     filenames = []
     for file in s:
@@ -35,7 +37,13 @@ def get_notebook_list():
             filenames.append(file['name'])
     return filenames
     
-notebook_list = get_notebook_list()
+# Skip notebook building on CI; doing the API request fails half the time,
+# because Github limits the number of requests that you (in this case Travis)
+# can do. We can create an API token and use that, but we can also just skip ...
+if os.getenv('TRAVIS', '').lower() == 'true':
+    notebook_list = []
+else:
+    notebook_list = get_notebook_list()
 
 
 def main():
diff --git a/flexx/app/_app.py b/flexx/app/_app.py
index 8576d3b..aeb34d6 100644
--- a/flexx/app/_app.py
+++ b/flexx/app/_app.py
@@ -22,32 +22,32 @@ class ExporterWebSocketDummy:
     by the app, so that these can be re-played in the exported document.
     """
     close_code = None
-    
+
     def __init__(self):
         self.commands = []
         self.ping_counter = 0
         # todo: make icon and title work
         #self.command('ICON %s.ico' % session.id)
-        # self.command('TITLE %s' % session._runtime_kwargs.get('title', 
+        # self.command('TITLE %s' % session._runtime_kwargs.get('title',
         #                                                       'Exported flexx app'))
-    
+
     def command(self, cmd):
         self.commands.append(cmd)
 
 
 class App:
     """ Specification of a Flexx app.
-    
+
     In the strict sense, this is a container for a Model class plus the
     args and kwargs that it is to be instantiated with.
-    
+
     Arguments:
         cls (Model): the Model class (or Widget) that represents this app.
         args: positional arguments used to instantiate the class (and received
             in its ``init()`` method).
         kwargs: keyword arguments used to initialize the model's properties.
     """
-    
+
     def __init__(self, cls, *args, **kwargs):
         if not isinstance(cls, type) and issubclass(type, Model):
             raise ValueError('App needs a Model class as its first argument.')
@@ -56,7 +56,7 @@ class App:
         self.kwargs = kwargs
         self._path = cls.__name__  # can be overloaded by serve()
         self._is_served = False
-        
+
         # Handle good defaults
         if hasattr(cls, 'title') and self.kwargs.get('title', None) is None:
             self.kwargs['title'] = 'Flexx app - ' + cls.__name__
@@ -66,30 +66,30 @@ class App:
                                                     'resources', 'flexx.ico'))
             icon_str = encodebytes(open(fname, 'rb').read()).decode()
             self.kwargs['icon'] = 'data:image/ico;base64,' + icon_str
-    
+
     def __call__(self, *args, **kwargs):
         a = list(self.args) + list(args)
         kw = {}
         kw.update(self.kwargs)
         kw.update(kwargs)
         return self.cls(*a, **kw)
-    
+
     def __repr__(self):
         t = '<App based on class %s pre-initialized with %i args and %i kwargs>'
         return t % (self.cls.__name__, len(self.args), len(self.kwargs))
-    
+
     @property
     def cls(self):
         """ The Model class that is the basis of this app.
         """
         return self._cls
-    
+
     @property
     def is_served(self):
         """ Whether this app is already registered by the app manager.
         """
         return self._is_served
-    
+
     @property
     def url(self):
         """ The url to acces this app. This raises an error if serve() has not
@@ -106,19 +106,19 @@ class App:
             host, port = server.serving
             path = self._path + '/' if self._path else ''
             return '%s://%s:%i/%s' % (proto, host, port, path)
-    
+
     @property
     def name(self):
         """ The name of the app, i.e. the url path that this app is served at.
         """
         return self._path or '__main__'
-    
+
     def serve(self, name=None):
         """ Start serving this app.
-        
+
         This registers the given class with the internal app manager. The
         app can be loaded via 'http://hostname:port/app_name'.
-        
+
         Arguments:
             name (str, optional): the relative URL path to serve the app on.
                 If this is ``''`` (the empty string), this will be the main app.
@@ -130,17 +130,17 @@ class App:
             self._path = name
         manager.register_app(self)
         self._is_served = True
-    
+
     def launch(self, runtime=None, **runtime_kwargs):
         """ Launch this app as a desktop app in the given runtime.
-        
+
         Arguments:
             runtime (str): the runtime to launch the application in.
                 Default 'app or browser'.
             runtime_kwargs: kwargs to pass to the ``webruntime.launch`` function.
                 A few names are passed to runtime kwargs if not already present
                 ('title' and 'icon').
-        
+
         Returns:
             app (Model): an instance of the given class.
         """
@@ -148,27 +148,27 @@ class App:
         if not self._is_served:
             self.serve()
         session = manager.create_session(self.name)
-        
+
         # Transfer title and icon
         if runtime_kwargs.get('title', None) is None and 'title' in self.kwargs:
             runtime_kwargs['title'] = self.kwargs['title']
         if runtime_kwargs.get('icon', None) is None and 'icon' in self.kwargs:
             runtime_kwargs['icon'] = self.kwargs['icon']
-        
+
         # Launch web runtime, the server will wait for the connection
         current_server()  # creates server if it did not yet exist
         url = self.url + '?session_id=%s' % session.id
         session._runtime = webruntime.launch(url, runtime=runtime, **runtime_kwargs)
         return session.app
-    
+
     def export(self, filename=None, link=None, write_shared=True):
         """ Export the given Model class to an HTML document.
-        
+
         Arguments:
             filename (str, optional): Path to write the HTML document to.
                 If not given or None, will return the html as a string.
             link (int): whether to link assets or embed them:
-            
+
                 * 0: all assets are embedded.
                 * 1: normal assets are embedded, remote assets remain remote.
                 * 2: all assets are linked (as separate files).
@@ -177,38 +177,38 @@ class App:
                 when linking to assets. This can be set to False when
                 exporting multiple apps to the same location. The shared assets can
                 then be exported last using ``app.assets.export(dirname)``.
-        
+
         Returns:
             html (str): The resulting html. If a filename was specified
             this returns None.
-        
+
         Notes:
             If the given filename ends with .hta, a Windows HTML Application is
             created.
         """
-        
+
         # Prepare name, based on exported file name (instead of cls.__name__)
         if not self._is_served:
             name = os.path.basename(filename).split('.')[0]
             name = name.replace('-', '_').replace(' ', '_')
             self.serve(name)
-        
+
         # Create session with id equal to the app name. This would not be strictly
         # necessary to make exports work, but it makes sure that exporting twice
         # generates the exact same thing (no randomly generated dir names).
         session = manager.create_session(self.name, self.name)
-        
+
         # Make fake connection using exporter object
         exporter = ExporterWebSocketDummy()
         manager.connect_client(exporter, session.app_name, session.id)
-        
+
         # Clean up again - NO keep in memory to ensure two sessions dont get same id
         # manager.disconnect_client(session)
-        
+
         # Warn if this app has data and is meant to be run standalone
         if (not link) and session.get_data_names():
             logger.warn('Exporting a standalone app, but it has registered data.')
-        
+
         # Get HTML - this may be good enough
         html = get_page_for_export(session, exporter.commands, link)
         if filename is None:
@@ -219,7 +219,7 @@ class App:
         elif not filename.lower().endswith(('.html', 'htm')):
             raise ValueError('Invalid extension for exporting to %r' %
                             os.path.basename(filename))
-        
+
         # Save to file. If standalone, all assets will be included in the main html
         # file, if not, we need to export shared assets and session assets too.
         filename = os.path.abspath(os.path.expanduser(filename))
@@ -229,7 +229,7 @@ class App:
             session._export_data(os.path.dirname(filename))
         with open(filename, 'wb') as f:
             f.write(html.encode())
-        
+
         app_type = 'standalone app' if link else 'app'
         logger.info('Exported %s to %r' % (app_type, filename))
 
@@ -243,21 +243,21 @@ def valid_app_name(name):
 
 class AppManager(event.HasEvents):
     """ Manage apps, or more specifically, the session objects.
-    
+
     There is one AppManager class (in ``flexx.model.manager``). It's
     purpose is to manage the application classes and instances. Intended
     for internal use.
     """
-    
+
     total_sessions = 0  # Keep track how many sessesions we've served in total
-    
+
     def __init__(self):
         super().__init__()
         # name -> (app, pending, connected) - lists contain Session objects
         self._appinfo = {}
         self._session_map = weakref.WeakValueDictionary()
         self._last_check_time = time.time()
-    
+
     def register_app(self, app):
         """ Register an app (an object that wraps a model class plus init args).
         After registering an app (and starting the server) it is
@@ -273,39 +273,39 @@ class AppManager(event.HasEvents):
             if app is not old_app:
                 logger.warn('Re-registering app class %r' % name)
         self._appinfo[name] = app, pending, connected
-    
+
     def create_default_session(self, cls=None):
         """ Create a default session for interactive use (e.g. the notebook).
         """
-        
+
         if '__default__' in self._appinfo:
             raise RuntimeError('The default session can only be created once.')
-        
+
         if cls is None:
             cls = Model
         if not isinstance(cls, type) and issubclass(cls, Model):
             raise TypeError('create_default_session() needs a Model subclass.')
-        
+
         # Create app and register it by __default__ name
         app = App(cls)
         app.serve('__default__')  # calls register_app()
-        
+
         # Create the session instance and register it
         session = Session('__default__')
         self._session_map[session.id] = session
         _, pending, connected = self._appinfo['__default__']
         pending.append(session)
-        
+
         # Instantiate the model
         model_instance = app(session=session, is_app=True)
         session._set_app(model_instance)
-        
+
         return session
-    
+
     def get_default_session(self):
         """ Get the default session that is used for interactive use.
         Returns None unless create_default_session() was called earlier.
-        
+
         When a Model class is created without a session, this method
         is called to get one (and will then fail if it's None).
         """
@@ -317,10 +317,10 @@ class AppManager(event.HasEvents):
             sessions = pending + connected
             if sessions:
                 return sessions[-1]
-    
+
     def _clear_old_pending_sessions(self):
         try:
-            
+
             count = 0
             for name in self._appinfo:
                 if name == '__default__':
@@ -334,34 +334,33 @@ class AppManager(event.HasEvents):
                 count += len(to_remove)
             if count:
                 logger.warn('Cleared %i old pending sessions' % count)
-        
+
         except Exception as err:
             logger.error('Error when clearing old pending sessions: %s' % str(err))
-    
-    def create_session(self, name, id=None, cookies=None):
+
+    def create_session(self, name, id=None, request=None):
         """ Create a session for the app with the given name.
-        
+
         Instantiate an app and matching session object corresponding
         to the given name, and return the session. The client should
         be connected later via connect_client().
         """
         # Called by the server when a client connects, and from the
         # launch and export functions.
-        
+
         if time.time() - self._last_check_time > 5:
             self._last_check_time = time.time()
             self._clear_old_pending_sessions()
-        
+
         if name == '__default__':
             raise RuntimeError('There can be only one __default__ session.')
         elif name not in self._appinfo:
             raise ValueError('Can only instantiate a session with a valid app name.')
-        
+
         app, pending, connected = self._appinfo[name]
-        
+
         # Create the session
-        session = Session(name)
-        session._set_cookies(cookies)
+        session = Session(name, request=request)
         if id is not None:
             session._id = id  # used by app.export
         self._session_map[session.id] = session
@@ -370,20 +369,20 @@ class AppManager(event.HasEvents):
         model_instance = app(session=session, is_app=True)
         # Session and app model need each-other, thus the _set_app()
         session._set_app(model_instance)
-        
+
         # Now wait for the client to connect. The client will be served
         # a page that contains the session_id. Upon connecting, the id
         # will be communicated, so it connects to the correct session.
         pending.append(session)
-        
+
         logger.debug('Instantiate app client %s' % session.app_name)
         return session
-    
+
     def connect_client(self, ws, name, session_id, cookies=None):
         """ Connect a client to a session that was previously created.
         """
         _, pending, connected = self._appinfo[name]
-        
+
         # Search for the session with the specific id
         for session in pending:
             if session.id == session_id:
@@ -392,7 +391,7 @@ class AppManager(event.HasEvents):
         else:
             raise RuntimeError('Asked for session id %r, but could not find it' %
                                session_id)
-    
+
         # Add app to connected, set ws
         assert session.status == Session.STATUS.PENDING
         logger.info('New session %s %s' % (name, session_id))
@@ -402,10 +401,10 @@ class AppManager(event.HasEvents):
         AppManager.total_sessions += 1
         self.connections_changed(session.app_name)
         return session  # For the ws
-    
+
     def disconnect_client(self, session):
         """ Close a connection to a client.
-        
+
         This is called by the websocket when the connection is closed.
         The manager will remove the session from the list of connected
         instances.
@@ -413,7 +412,7 @@ class AppManager(event.HasEvents):
         if session.app_name == '__default__':
             logger.info('Default session lost connection to client.')
             return  # The default session awaits a re-connect
-        
+
         _, pending, connected = self._appinfo[session.app_name]
         try:
             connected.remove(session)
@@ -422,7 +421,7 @@ class AppManager(event.HasEvents):
         logger.info('Session closed %s %s' %(session.app_name, session.id))
         session.close()
         self.connections_changed(session.app_name)
-    
+
     def has_app_name(self, name):
         """ Returns the case-corrected name if the given name matches
         a registered appliciation (case insensitive). Returns None if the
@@ -434,23 +433,23 @@ class AppManager(event.HasEvents):
                 return key
         else:
             return None
-    
+
     def get_app_names(self):
         """ Get a list of registered application names.
         """
         return [name for name in sorted(self._appinfo.keys())]
-    
+
     def get_session_by_id(self, id):
         """ Get session object by its id
         """
         return self._session_map.get(id, None)
-    
+
     def get_connections(self, name):
         """ Given an app name, return the session connected objects.
         """
         _, pending, connected = self._appinfo[name]
         return list(connected)
-    
+
     @event.emitter
     def connections_changed(self, name):
         """ Emits an event with the name of the app for which a
diff --git a/flexx/app/_model.py b/flexx/app/_model.py
index 8157127..9f5c30e 100644
--- a/flexx/app/_model.py
+++ b/flexx/app/_model.py
@@ -353,8 +353,6 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
         session = kwargs.pop('session', None)
         kwargs.pop('is_app', None)
         
-        self._disposed = False
-        
         # Init session
         if session is None:
             active_model = get_active_model()
@@ -468,7 +466,6 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
             except Exception:
                 pass  # ws can be closed/gone if this gets invoked from __del__
         super().dispose()
-        self._disposed = True
     
     @property
     def id(self):
diff --git a/flexx/app/_session.py b/flexx/app/_session.py
index a4825f5..2027dad 100644
--- a/flexx/app/_session.py
+++ b/flexx/app/_session.py
@@ -25,11 +25,11 @@ reprs = json.dumps
 
 # Use the system PRNG for session id generation (if possible)
 # NOTE: secure random string generation implementation is adapted
-#       from the Django project. 
+#       from the Django project.
 
 def get_random_string(length=24, allowed_chars=None):
     """ Produce a securely generated random string.
-    
+
     With a length of 12 with the a-z, A-Z, 0-9 character set returns
     a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
     """
@@ -50,89 +50,103 @@ class Session:
     """ A session between Python and the client runtime.
     This class is what holds together the app widget, the web runtime,
     and the websocket instance that connects to it.
-    
+
     Responsibilities:
-    
+
     * Send messages to the client and parse messages received by the client.
     * Keep track of Model instances associated with the session.
     * Ensure that the client has all the module definitions it needs.
     * Allow the user to send data to the client.
-    
+
     """
-    
+
     STATUS = new_type('Enum', (), {'PENDING': 1, 'CONNECTED': 2, 'CLOSED': 0})
-    
-    def __init__(self, app_name, store=None):  # Allow custom store for testing
+
+    def __init__(self, app_name, store=None,
+                 request=None):  # Allow custom store for testing
         self._store = store if (store is not None) else assetstore
         assert isinstance(self._store, AssetStore)
-        self._cookies = {}
-        
+
         self._creation_time = time.time()  # used by app manager
-        
+
         # Id and name of the app
         self._id = get_random_string()
         self._app_name = app_name
-        
+
         # To keep track of what modules are defined at the client
         self._present_classes = set()  # Model classes known by the client
         self._present_modules = set()  # module names that, plus deps
         self._present_assets = set()  # names of used associated assets
         self._assets_to_ignore = set()  # user settable
-        
+
         # Data for this session (in addition to the data provided by the store)
         self._data = {}
         self._data_volatile = {}  # deleted after retrieving
-        
+
         # More vars
         self._runtime = None  # init web runtime, will be set when used
         self._ws = None  # init websocket, will be set when a connection is made
         self._model = None  # Model instance, can be None if app_name is __default__
         self._closing = False  # Flag to help with shutdown
-        
+
         # The session assigns model id's, keeps track of model objects and
         # sometimes keeps them alive for a short while.
         self._model_counter = 0
         self._model_instances = weakref.WeakValueDictionary()
         self._instances_guarded = {}  # id: (ping_count, instance)
         self._roundtrip_based_calllaters = []  # (ping_count, callback, args, kwargs)
-        
+
         # While the client is not connected, we keep a queue of
         # commands, which are send to the client as soon as it connects
         self._pending_commands = []
-    
+
+        # request related information
+        self._request = request
+        if request and request.cookies:
+            cookies = request.cookies
+        else:
+            cookies = {}
+        self._set_cookies(cookies)
+
     def __repr__(self):
         t = '<%s for %r (%i) at 0x%x>'
         return t % (self.__class__.__name__, self.app_name, self.status, id(self))
-    
+
+    @property
+    def request(self):
+        """The tornado request that was at the origin of this session
+        """
+        return self._request
+
     @property
     def id(self):
         """ The unique identifier of this session.
         """
         return self._id
-    
+
     @property
     def app_name(self):
         """ The name of the application that this session represents.
         """
         return self._app_name
-    
+
     @property
     def app(self):
         """ The Model instance that represents the app.
         """
         return self._model
-    
+
     @property
     def runtime(self):
         """ The runtime that is rendering this app instance. Can be
         None if the client is a browser.
         """
         return self._runtime
-    
+
     @property
     def status(self):
         """ The status of this session. The lifecycle for each session is:
-        
+
         * status 1: pending
         * statys 2: connected
         * status 0: closed
@@ -143,13 +157,13 @@ class Session:
             return self.STATUS.CONNECTED  # alive and kicking
         else:
             return self.STATUS.CLOSED  # connection closed
-    
+
     @property
     def present_modules(self):
         """ The set of module names that is (currently) available at the client.
         """
         return set(self._present_modules)
-    
+
     @property
     def assets_to_ignore(self):
         """ The set of names of assets that should *not* be pushed to
@@ -157,7 +171,7 @@ class Session:
         Add names to this set to prevent them from being loaded.
         """
         return self._assets_to_ignore
-    
+
     def close(self):
         """ Close the session: close websocket, close runtime, dispose app.
         """
@@ -167,7 +181,7 @@ class Session:
         self._roundtrip_based_calllaters = []
         self._closing = True  # suppress warnings for session being closed.
         try:
-            
+
             # Close the websocket
             if self._ws:
                 self._ws.close_this()
@@ -183,9 +197,9 @@ class Session:
             self._data_volatile = {}
         finally:
             self._closing = False
-    
+
     ## Hooking up with app, websocket, runtime
-    
+
     def _set_ws(self, ws):
         """ A session is always first created, so we know what page to
         serve. The client will connect the websocket, and communicate
@@ -204,7 +218,7 @@ class Session:
         for command in self._pending_commands:
             self._ws.command(command)
         self._ws.command('INIT-DONE')
-   
+
     def _set_cookies(self, cookies=None):
         """ To set cookies, must be an http.cookie.SimpleCookie object.
         When the app is loaded as a web app, the cookies are set *before* the
@@ -212,19 +226,19 @@ class Session:
         is connected.
         """
         self._cookies = cookies if cookies else SimpleCookie()
-    
+
     def _set_app(self, model):
         if self._model is not None:
             raise RuntimeError('Session already has an associated Model.')
         self._model = model
-    
+
     def _set_runtime(self, runtime):
         if self._runtime is not None:
             raise RuntimeError('Session already has a runtime.')
         self._runtime = runtime
-    
+
     ## Cookies, mmm
-    
+
     def get_cookie(self, name, default=None, max_age_days=31, min_version=None):
         """ Gets the value of the cookie with the given name, else default.
         Note that cookies only really work for web apps.
@@ -238,7 +252,7 @@ class Session:
             return value.decode()
         else:
             return default
-    
+
     def set_cookie(self, name, value, expires_days=30, version=None,
                    domain=None, expires=None, path="/", **kwargs):
         """ Sets the given cookie name/value with the given options. Set value
@@ -249,13 +263,13 @@ class Session:
         """
         # This code is taken (in modified form) from the Tornado project
         # Copyright 2009 Facebook
-        # Licensed under the Apache License, Version 2.0 
-        
+        # Licensed under the Apache License, Version 2.0
+
         # Assume tornado is available ...
         from tornado.escape import native_str
         from tornado.httputil import format_timestamp
         from tornado.web import create_signed_value
-        
+
         # Clear cookie?
         if value is None:
             value = ""
@@ -264,7 +278,7 @@ class Session:
             secret = config.cookie_secret
             value = create_signed_value(secret, name, value, version=version,
                                         key_version=None)
-        
+
         # The cookie library only accepts type str, in both python 2 and 3
         name = native_str(name)
         value = native_str(value)
@@ -292,12 +306,12 @@ class Session:
             if k in ['httponly', 'secure'] and not v:
                 continue
             morsel[k] = v
-        
+
         self._exec('document.cookie = "%s";' %
                    morsel.OutputString().replace('"', '\\"'))
-    
+
     ## Data
-    
+
     def _send_data(self, id, data, meta):
         """ Send data to a model on the JS side. The corresponding object's
         receive_data() method is called when the data is available in JS.
@@ -335,22 +349,22 @@ class Session:
         else:
             raise TypeError('session.send_data() data must be a bytes or a URL, '
                             'not %s.' % data.__class__.__name__)
-        
+
         # Tell JS to retrieve data
         t = 'window.flexx.instances.%s.retrieve_data("%s", %s);'
         self._exec(t % (id, url, reprs(meta)))
-    
+
     def add_data(self, name, data):
         """ Add data to serve to the client (e.g. images), specific to this
         session. Returns the link at which the data can be retrieved.
         See ``Session.send_data()`` for a send-and-forget mechanism, and
         ``app.assets.add_shared_data()`` to provide shared data.
-        
+
         Parameters:
             name (str): the name of the data, e.g. 'icon.png'. If data has
                 already been set on this name, it is overwritten.
             data (bytes): the data blob.
-        
+
         Returns:
             url: the (relative) url at which the data can be retrieved.
         """
@@ -362,19 +376,19 @@ class Session:
             raise TypeError('Session.add_data() data must be bytes.')
         self._data[name] = data
         return '_data/%s/%s' % (self.id, name)  # relative path so it works /w export
-    
+
     def remove_data(self, name):
         """ Remove the data associated with the given name. If you need this,
         also consider ``send_data()``. Also note that data is automatically
         released when the session is closed.
         """
         self._data.pop(name, None)
-    
+
     def get_data_names(self):
         """ Get a list of names of the data provided by this session.
         """
         return list(self._data.keys())
-    
+
     def get_data(self, name):
         """ Get the data corresponding to the given name. This can be
         data local to the session, or global data. Returns None if data
@@ -387,7 +401,7 @@ class Session:
         if data is None:
             data = self._store.get_data(name)
         return data
-    
+
     def _export_data(self, dirname, clear=False):
         """ Export all assets and data specific to this session.
         Private method, used by app.export().
@@ -399,7 +413,7 @@ class Session:
         logger.info('Exported data for %r to %r.' % (self.id, dirname))
 
     ## Keeping track of model objects
-    
+
     def _register_model(self, model):
         """ Called by Model to give them an id and register with the session.
         """
@@ -413,7 +427,7 @@ class Session:
         self._model_instances[model.id] = model
         # Register the class to that the client has the needed definitions
         self._register_model_class(cls)
-    
+
     def get_model_instance_by_id(self, id):
         """ Get instance of Model class corresponding to the given id,
         or None if it does not exist.
@@ -424,7 +438,7 @@ class Session:
             t = 'Model instance %r does not exist in this session (anymore).'
             logger.warn(t % id)
             return None  # Could we revive it? ... probably not a good idea
-    
+
     def keep_alive(self, ob, iters=4):
         """ Keep an object alive for a certain amount of time, expressed
         in Python-JS ping roundtrips. This is intended for making Model
@@ -436,7 +450,7 @@ class Session:
         lifetime = counter + int(iters)
         if lifetime > self._instances_guarded.get(obid, (0, ))[0]:
             self._instances_guarded[obid] = lifetime, ob
-    
+
     def call_after_roundtrip(self, callback, *args, **kwargs):
         """ A variant of ``app.call_later()`` that calls a callback after
         a py-js roundrip. This can be convenient to delay an action until
@@ -446,13 +460,13 @@ class Session:
         lifetime = counter + 2  # a couple of roundtrips, to be safe
         entry = lifetime, callback, args, kwargs
         self._roundtrip_based_calllaters.append(entry)
-    
+
     ## JIT asset definitions
-    
+
     # todo: deprecated - remove this
     def register_model_class(self, cls):
         logger.warn('register_model_class() is no more.')
-    
+
     def _register_model_class(self, cls):
         """ Mark the given Model class as used; ensure that the client
         knows about the module that it is defined in, dependencies
@@ -463,7 +477,7 @@ class Session:
         # Early exit if we know the class already
         if cls in self._present_classes:
             return
-        
+
         # Make sure that no two models have the same name, or we get problems
         # that are difficult to debug. Unless classes are defined interactively.
         # The modules of classes that are re-registered are re-defined. The base
@@ -480,38 +494,38 @@ class Session:
                 raise RuntimeError('Cannot have multiple Model classes with the same '
                                    'name unless using interactive session and the '
                                    'classes are dynamically defined: %r' % same_name)
-        
+
         # Mark the class and the module as used
         logger.debug('Registering Model class %r' % cls.__name__)
         self._register_module(cls.__jsmodule__, True)
-    
+
     def _register_module(self, mod_name, force=False):
         """ Register a module with the client, as well as its
         dependencies, and associated assests of the module and its
         dependencies. If the module was already defined, it is
         re-defined.
         """
-        
+
         modules = set()
         assets = []
-        
+
         def collect_module_and_deps(mod):
             if mod.name.startswith('flexx.app'):
                 return  # these are part of flexx-core asset
-            if mod.name not in self._present_modules: 
+            if mod.name not in self._present_modules:
                 self._present_modules.add(mod.name)
                 for dep in mod.deps:
                     submod = self._store.modules[dep]
                     collect_module_and_deps(submod)
                 modules.add(mod)
-        
+
         # Collect module and dependent modules that are not yet defined
         self._store.update_modules()  # Ensure up-to-date module definition
         mod = self._store.modules[mod_name]
         collect_module_and_deps(mod)
         f = lambda m: (m.name.startswith('__main__'), m.name)
         modules = solve_dependencies(sorted(modules, key=f))
-        
+
         # Collect associated assets
         for mod in modules:
             for asset_name in self._store.get_associated_assets(mod.name):
@@ -528,12 +542,12 @@ class Session:
                 assets.append(self._store.get_asset(mod.name + '.css'))
         for mod in modules:
             assets.append(self._store.get_asset(mod.name + '.js'))
-        
+
         # Mark classes as used
         for mod in modules:
             for cls in mod.model_classes:
                 self._present_classes.add(cls)
-        
+
         # Push assets over the websocket. Note how this works fine with the
         # notebook because we turn ws commands into display(HTML()).
         # JS can be defined via eval() or by adding a <script> to the DOM.
@@ -550,9 +564,9 @@ class Session:
                 suffix = 'JS-EVAL'
             t = 'DEFINE-%s %s %s'
             self._send_command(t % (suffix, asset.name, asset.to_string()))
-    
+
     ## Communication with the client
-    
+
     def _send_command(self, command):
         """ Send the command, add to pending queue.
         """
@@ -565,7 +579,7 @@ class Session:
         else:
             #raise RuntimeError('Cannot send commands; app is closed')
             logger.warn('Cannot send commands; app is closed')
-    
+
     def _receive_command(self, command):
         """ Received a command from JS.
         """
@@ -597,7 +611,7 @@ class Session:
                 ob._emit_from_js(name, txt)
         else:
             logger.warn('Unknown command received from JS:\n%s' % command)
-    
+
     def _receive_pong(self, count):
         """ Called by ws when it gets a pong. Thus gets called about
         every sec. Clear the guarded Model instances for which the
@@ -607,21 +621,21 @@ class Session:
                            self._instances_guarded.values() if c <= count]
         for ob in objects_to_clear:
             self._instances_guarded.pop(id(ob))
-        
+
         for i in reversed(range(len(self._roundtrip_based_calllaters))):
             if self._roundtrip_based_calllaters[i][0] <= count:
                 entry = self._roundtrip_based_calllaters.pop(i)
                 lifetime, callback, args, kwargs = entry
                 call_later(0, callback, *args, **kwargs)
-    
+
     def _exec(self, code):
         """ Like eval, but without returning the result value.
         """
         self._send_command('EXEC ' + code)
-    
+
     def eval(self, code):
         """ Evaluate the given JavaScript code in the client
-        
+
         Intended for use during development and debugging. Deployable
         code should avoid making use of this function.
         """
@@ -679,7 +693,7 @@ def get_page_for_export(session, commands, link=0):
     # Create a session asset for it, "-export.js" is always embedded
     export_asset = Asset('flexx-export.js', '\n'.join(lines))
     js_assets.append(export_asset)
-    
+
     return _get_page(session, js_assets, css_assets, link, True)
 
 
@@ -687,12 +701,12 @@ def _get_page(session, js_assets, css_assets, link, export):
     """ Compose index page.
     """
     pre_path = '_assets' if export else '/flexx/assets'
-    
+
     codes = []
-    
+
     t = 'var flexx = {app_name: "%s", session_id: "%s"};'
     codes.append('<script>%s</script>\n' % t % (session.app_name, session.id))
-    
+
     for assets in [css_assets, js_assets]:
         for asset in assets:
             if not link:
@@ -706,7 +720,7 @@ def _get_page(session, js_assets, css_assets, link, export):
             if export and assets is js_assets:
                 codes.append('<script>window.flexx.spin();</script>')
         codes.append('')  # whitespace between css and js assets
-    
+
     src = INDEX
     if not link:
         asset_names = [a.name for a in css_assets + js_assets]
@@ -715,5 +729,5 @@ def _get_page(session, js_assets, css_assets, link, export):
         src = src.replace('ASSET-HOOK', '\n\n\n'.join(codes))
     else:
         src = src.replace('ASSET-HOOK', '\n'.join(codes))
-    
+
     return src
diff --git a/flexx/app/_tornadoserver.py b/flexx/app/_tornadoserver.py
index 3b9f80c..faf197f 100644
--- a/flexx/app/_tornadoserver.py
+++ b/flexx/app/_tornadoserver.py
@@ -46,14 +46,14 @@ def is_main_thread():
 class TornadoServer(AbstractServer):
     """ Flexx Server implemented in Tornado.
     """
-    
+
     def __init__(self, host, port, new_loop, **kwargs):
         self._new_loop = new_loop
         self._app = None
         self._server = None
         self._get_io_loop()
         super().__init__(host, port, **kwargs)
-    
+
     def _get_io_loop(self):
         # Get a new ioloop or the current ioloop for this thread
         if self._new_loop:
@@ -62,9 +62,9 @@ class TornadoServer(AbstractServer):
             self._loop = IOLoop.current(instance=is_main_thread())
             if self._loop is None:
                 self._loop = IOLoop(make_current=True)
-    
+
     def _open(self, host, port, **kwargs):
-        # Note: does not get called if host is False. That way we can 
+        # Note: does not get called if host is False. That way we can
         # run Flexx in e.g. JLab's application.
 
         # handle ssl, wether from configuration or given args
@@ -114,14 +114,14 @@ class TornadoServer(AbstractServer):
                 [sock] = netutil.bind_sockets(None, host, family=socket.AF_INET)
                 self._server.add_sockets([sock])
                 port = sock.getsockname()[1]
-        
+
         # Notify address, so its easy to e.g. copy and paste in the browser
         self._serving = self._app._flexx_serving = host, port
         proto = 'http'
         if 'ssl_options' in kwargs:
             proto = 'https'
         logger.info('Serving apps at %s://%s:%i/' % (proto, host, port))
-    
+
     def _start(self):
         # Ensure that our loop is the current loop for this thread
         if self._new_loop:
@@ -134,15 +134,15 @@ class TornadoServer(AbstractServer):
         # AbstractServer class keeps track of this.
         if not getattr(self._loop, '_in_event_loop', False):
             self._loop.start()
-    
+
     def _stop(self):
         # todo: explicitly close all websocket connections
         logger.debug('Stopping Tornado server')
         self._loop.stop()
-    
+
     def _close(self):
         self._server.stop()
-    
+
     def call_later(self, delay, callback, *args, **kwargs):
         # We use a wrapper func so that exceptions are processed via our
         # logging system. Also fixes that Tornado seems to close websockets
@@ -155,22 +155,22 @@ class TornadoServer(AbstractServer):
             except Exception as err:
                 err.skip_tb = 1
                 logger.exception(err)
-        
+
         if delay <= 0:
             self._loop.add_callback(wrapper)
         else:
             self._loop.call_later(delay, wrapper)
-    
+
     @property
     def app(self):
         """ The Tornado Application object being used."""
         return self._app
-    
+
     @property
     def loop(self):
         """ The Tornado IOLoop object being used."""
         return self._loop
-    
+
     @property
     def server(self):
         """ The Tornado HttpServer object being used."""
@@ -186,7 +186,7 @@ class TornadoServer(AbstractServer):
 
 def port_hash(name):
     """ Given a string, returns a port number between 49152 and 65535
-    
+
     This range (of 2**14 posibilities) is the range for dynamic and/or
     private ports (ephemeral ports) specified by iana.org. The algorithm
     is deterministic.
@@ -202,11 +202,11 @@ def port_hash(name):
 class FlexxHandler(RequestHandler):
     """ Base class for Flexx' Tornado request handlers.
     """
-    
+
     def initialize(self, **kwargs):
         # kwargs == dict set as third arg in url spec
         pass
-    
+
     def write_error(self, status_code, **kwargs):
         if status_code == 404:  # does not work?
             self.write('flexx.ui wants you to connect to root (404)')
@@ -221,7 +221,7 @@ class FlexxHandler(RequestHandler):
                     pass
                 self.write(msg)
             super().write_error(status_code, **kwargs)
-    
+
     def on_finish(self):
         pass
 
@@ -229,15 +229,15 @@ class FlexxHandler(RequestHandler):
 class AppHandler(FlexxHandler):
     """ Handler for http requests to get apps.
     """
-    
+
     @gen.coroutine
     def get(self, full_path):
-        
+
         logger.debug('Incoming request at %r' % full_path)
-        
+
         ok_app_names = '__main__', '__default__', '__index__'
         parts = [p for p in full_path.split('/') if p]
-        
+
         # Try getting regular app name
         # Note: invalid part[0] can mean its a path relative to the main app
         app_name = None
@@ -248,7 +248,7 @@ class AppHandler(FlexxHandler):
             if parts[0] in ok_app_names or manager.has_app_name(parts[0]):
                 app_name = parts[0]
                 path = '/'.join(parts[1:])
-        
+
         # If it does not look like an app, it might be that the request is for
         # the main app. The main app can have sub-paths, but lets try to filter
         # out cases that might make Flexx unnecessarily instantiate an app.
@@ -258,13 +258,13 @@ class AppHandler(FlexxHandler):
                 return self.redirect('/flexx/data/' + full_path)
             # If we did not return ... assume this is the default app
             app_name = '__main__'
-        
+
         # Try harder to produce an app
         if app_name == '__main__':
             app_name = manager.has_app_name('__main__')
         elif '/' not in full_path:
             return self.redirect('/%s/' % app_name)  # ensure slash behind name
-        
+
         # Maybe the user wants an index? Otherwise error.
         if not app_name:
             if not parts:
@@ -272,7 +272,7 @@ class AppHandler(FlexxHandler):
             else:
                 name = parts[0] if parts else '__main__'
                 return self.write('No app "%s" is currently hosted.' % name)
-    
+
         # We now have:
         # * app_name: name of the app, must be a valid identifier, names
         #   with underscores are reserved for special things like assets,
@@ -282,36 +282,33 @@ class AppHandler(FlexxHandler):
             self._get_index(app_name, path)  # Index page
         else:
             self._get_app(app_name, path)  # An actual app!
-    
+
     def _get_index(self, app_name, path):
         if path:
             return self.redirect('/flexx/__index__')
-        all_apps = ['<li><a href="%s/">%s</a></li>' % (name, name) for name in 
+        all_apps = ['<li><a href="%s/">%s</a></li>' % (name, name) for name in
                     manager.get_app_names()]
         the_list = '<ul>%s</ul>' % ''.join(all_apps) if all_apps else 'no apps'
         self.write('Index of available apps: ' + the_list)
-    
+
     def _get_app(self, app_name, path):
-    
-        # todo: send path to app somehow
-        
         # Allow serving data/assets relative to app so that data can use
         # relative paths just like exported apps.
         if path.startswith(('_data/', '_assets/')):
             return self.redirect('/flexx/' + path[1:])
-        
+
         # Get case-corrected app name if the app is known
         correct_app_name = manager.has_app_name(app_name)
-        
+
         # Error or redirect if app name is not right
         if not correct_app_name:
             return self.write('No app "%s" is currently hosted.' % app_name)
         if correct_app_name != app_name:
             return self.redirect('/%s/%s' % (correct_app_name, path))
-        
+
         # Should we bind this app instance to a pre-created session?
         session_id = self.get_argument('session_id', '')
-        
+
         if session_id:
             # If session_id matches a pending app, use that session
             session = manager.get_session_by_id(session_id)
@@ -321,7 +318,7 @@ class AppHandler(FlexxHandler):
                 self.redirect('/%s/' % app_name)  # redirect for normal serve
         else:
             # Create session - websocket will connect to it via session_id
-            session = manager.create_session(app_name, cookies=self.cookies)
+            session = manager.create_session(app_name, request=self.request)
             self.write(get_page(session).encode())
 
 
@@ -336,12 +333,12 @@ class MainHandler(RequestHandler):
         guess = mimetypes.guess_type(fname)[0]
         if guess:
             self.set_header("Content-Type", guess)
-    
+
     @gen.coroutine
     def get(self, full_path):
-        
+
         logger.debug('Incoming request at %s' % full_path)
-        
+
         # Analyze path to derive components
         # Note: invalid app name can mean its a path relative to the main app
         parts = [p for p in full_path.split('/') if p]
@@ -349,7 +346,7 @@ class MainHandler(RequestHandler):
             return self.write('Root url for flexx: assets, assetview, data, cmd')
         selector = parts[0]
         path = '/'.join(parts[1:])
-        
+
         if selector in ('assets', 'assetview', 'data'):
             self._get_asset(selector, path)  # JS, CSS, or data
         elif selector == 'info':
@@ -358,34 +355,34 @@ class MainHandler(RequestHandler):
             self._get_cmd(selector, path)  # Execute (or ignore) command
         else:
             return self.write('Invalid url path "%s".' % full_path)
-    
+
     def _get_asset(self, selector, path):
-        
+
         # Get session id and filename
         session_id, _, filename = path.partition('/')
         session_id = '' if session_id == 'shared' else session_id
-        
+
         # Get asset provider: store or session
         asset_provider = assets
         if session_id and selector != 'data':
             return self.write('Only supports shared assets, not ' % filename)
         elif session_id:
             asset_provider = manager.get_session_by_id(session_id)
-        
+
         # Checks
         if asset_provider is None:
             return self.write('Invalid session %r' % session_id)
         if not filename:
             return self.write('Root dir for %s/%s' % (selector, path))
-        
+
         if selector == 'assets':
-            
+
             # If colon: request for a view of an asset at a certain line
             if '.js:' in filename or '.css:' in filename or filename[0] == ':':
                 fname, where = filename.split(':')[:2]
                 return self.redirect('/flexx/assetview/%s/%s#L%s' %
                     (session_id or 'shared', fname.replace('/:', ':'), where))
-            
+
             # Retrieve asset
             try:
                 res = asset_provider.get_asset(filename)
@@ -394,9 +391,9 @@ class MainHandler(RequestHandler):
             else:
                 self._guess_mime_type(filename)
                 self.write(res.to_string())
-        
+
         elif selector == 'assetview':
-            
+
             # Retrieve asset
             try:
                 res = asset_provider.get_asset(filename)
@@ -404,7 +401,7 @@ class MainHandler(RequestHandler):
                 return self.write('Could not load asset %r' % filename)
             else:
                 res = res.to_string()
-            
+
             # Build HTML page
             style = ('pre {display:block; width: 100%; padding:0; margin:0;} '
                     'a {text-decoration: none; color: #000; background: #ddd;} '
@@ -417,10 +414,10 @@ class MainHandler(RequestHandler):
                              (i+1, i+1, str(i+1).rjust(4).replace(' ', '&nbsp'), line))
             lines.append('</body></html>')
             return self.write('\n'.join(lines))
-        
+
         elif selector == 'data':
             # todo: can/do we async write in case the data is large?
-            
+
             # Retrieve data
             res = asset_provider.get_data(filename)
             if res is None:
@@ -428,10 +425,10 @@ class MainHandler(RequestHandler):
             else:
                 self._guess_mime_type(filename)  # so that images show up
                 return self.write(res)
-        
+
         else:
             raise RuntimeError('Invalid asset type %r' % selector)
-    
+
     def _get_info(self, selector, info):
         """ Provide some rudimentary information about the server.
         Note that this is publicly accesible.
@@ -440,22 +437,22 @@ class MainHandler(RequestHandler):
         napps = len(manager.get_app_names())
         nsessions = sum([len(manager.get_connections(x))
                          for x in manager.get_app_names()])
-        
+
         info = []
         info.append('Runtime: %1.1f s' % runtime)
         info.append('Number of apps: %i' % napps)
         info.append('Number of sessions: %i' % nsessions)
-        
+
         info = '\n'.join(['<li>%s</li>' % i for i in info])
         self.write('<ul>' + info + '</ul>')
-    
+
     def _get_cmd(self, selector, path):
         """ Allow control of the server using http, but only from localhost!
         """
         if not self.request.host.startswith('localhost:'):
             self.write('403')
             return
-        
+
         if not path:
             self.write('No command given')
         elif path == 'info':
@@ -477,19 +474,19 @@ class MessageCounter:
     """ Simple class to count incoming messages and periodically log
     the number of messages per second.
     """
-    
+
     def __init__(self):
         self._collect_interval = 0.2  # period over which to collect messages
         self._notify_interval = 3.0  # period on which to log the mps
         self._window_interval = 4.0  # size of sliding window
-        
+
         self._mps = [(time.time(), 0)]  # tuples of (time, count)
         self._collect_count = 0
         self._collect_stoptime = 0
-        
+
         self._stop = False
         self._notify()
-    
+
     def trigger(self):
         t = time.time()
         if t < self._collect_stoptime:
@@ -498,7 +495,7 @@ class MessageCounter:
             self._mps.append((self._collect_stoptime, self._collect_count))
             self._collect_count = 1
             self._collect_stoptime = t + self._collect_interval
-    
+
     def _notify(self):
         mintime = time.time() - self._window_interval
         self._mps = [x for x in self._mps if x[0] > mintime]
@@ -508,11 +505,11 @@ class MessageCounter:
         else:
             n, T = 0, self._collect_interval
         logger.debug('Websocket messages per second: %1.1f' % (n / T))
-        
+
         if not self._stop:
             loop = IOLoop.current()
             loop.call_later(self._notify_interval, self._notify)
-    
+
     def stop(self):
         self._stop = True
 
@@ -520,50 +517,50 @@ class MessageCounter:
 class WSHandler(WebSocketHandler):
     """ Handler for websocket.
     """
-    
+
     # https://tools.ietf.org/html/rfc6455#section-7.4.1
-    known_reasons = {1000: 'client done', 
-                     1001: 'client closed', 
-                     1002: 'protocol error', 
+    known_reasons = {1000: 'client done',
+                     1001: 'client closed',
+                     1002: 'protocol error',
                      1003: 'could not accept data',
                      }
-    
+
     # --- callbacks
-    
+
     def open(self, path=None):
         """ Called when a new connection is made.
         """
         if not hasattr(self, 'close_code'):  # old version of Tornado?
             self.close_code, self.close_reason = None, None
-        
+
         self._session = None
         self._mps_counter = MessageCounter()
-        
+
         # Don't collect messages to send them more efficiently, just send asap
         # self.set_nodelay(True)
-        
+
         if isinstance(path, bytes):
             path = path.decode()
         self.app_name = path.strip('/')
-        
+
         logger.debug('New websocket connection %s' % path)
         if manager.has_app_name(self.app_name):
             IOLoop.current().spawn_callback(self.pinger1)
             IOLoop.current().spawn_callback(self.pinger2)
         else:
             self.close(1003, "Could not associate socket with an app.")
-    
+
     # todo: @gen.coroutine?
     def on_message(self, message):
         """ Called when a new message is received from JS.
-        
+
         This handles one message per event loop iteration.
-        
+
         We now have a very basic protocol for receiving messages,
         we should at some point define a real formalized protocol.
         """
         self._mps_counter.trigger()
-        
+
         self._pongtime = time.time()
         if self._session is None:
             if message.startswith('hiflexx '):
@@ -584,7 +581,7 @@ class WSHandler(WebSocketHandler):
             except Exception as err:
                 err.skip_tb = 1
                 logger.exception(err)
-    
+
     def on_close(self):
         """ Called when the connection is closed.
         """
@@ -595,30 +592,30 @@ class WSHandler(WebSocketHandler):
         if self._session is not None:
             manager.disconnect_client(self._session)
             self._session = None  # Allow cleaning up
-    
+
     @gen.coroutine
     def pinger1(self):
         """ Check for timeouts. This helps remove lingering false connections.
-        
+
         This uses the websocket's native ping-ping mechanism. On the
         browser side, pongs work even if JS is busy. On the Python side
-        we perform a check whether we were really waiting or whether Python 
+        we perform a check whether we were really waiting or whether Python
         was too busy to detect the pong.
         """
         self._pongtime = time.time()
         self._pingtime = pingtime = 0
-        
+
         while self.close_code is None:
             dt = config.ws_timeout
-            
+
             # Ping, but don't spam
             if pingtime <= self._pongtime:
                 self.ping(b'x')
                 pingtime = self._pingtime = time.time()
                 iters_since_ping = 0
-            
+
             yield gen.sleep(dt / 5)
-            
+
             # Check pong status
             iters_since_ping += 1
             if iters_since_ping < 5:
@@ -629,28 +626,28 @@ class WSHandler(WebSocketHandler):
                 logger.warn('Closing connection due to lack of pong')
                 self.close(1000, 'Conection timed out (no pong).')
                 return
-    
+
     def on_pong(self, data):
         """ Implement the ws's on_pong() method. Called when our ping
         is returned by the browser.
         """
         self._pongtime = time.time()
-    
+
     @property
     def ping_counter(self):
         """ Counter indicating the number of pings so far. This measure is
         used by ``Session.keep_alive()``.
         """
         return self._ping_counter
-    
+
     @gen.coroutine
     def pinger2(self):
         """ Ticker so we have a signal of sorts to indicate round-trips.
-        
+
         This is used to implement session.call_on_next_pong(), which
         is sort of like call_later(), but waits for both Py and JS to "flush"
         their current events.
-        
+
         This uses a ping-pong mechanism implemented *atop* the websocket.
         When JS is working, it is not able to send a pong (which is what we
         want in this case).
@@ -662,44 +659,44 @@ class WSHandler(WebSocketHandler):
                 self._ping_counter += 1
                 self.command('PING %i' % self._ping_counter)
             yield gen.sleep(1.0)
-    
+
     def on_pong2(self, data):
         """ Called when our ping is returned by Flexx.
         """
         self._pong_counter = int(data)
         if self._session:
             self._session._receive_pong(self._pong_counter)
-    
+
     # --- methods
-    
+
     def command(self, cmd):
         self.write_message(cmd, binary=BINARY)
-    
+
     def close(self, *args):
         try:
             WebSocketHandler.close(self, *args)
         except TypeError:
             WebSocketHandler.close(self)  # older Tornado
-    
+
     def close_this(self):
         """ Call this to close the websocket
         """
         self.close(1000, 'closed by server')
-    
+
     def check_origin(self, origin):
         """ Handle cross-domain access; override default same origin policy.
         """
         # http://www.tornadoweb.org/en/stable/_modules/tornado/websocket.html
         #WebSocketHandler.check_origin
-        
+
         serving_host = self.request.headers.get("Host")
         serving_hostname, _, serving_port = serving_host.partition(':')
         connecting_host = urlparse(origin).netloc
         connecting_hostname, _, connecting_port = connecting_host.partition(':')
-        
+
         serving_port = serving_port or '80'
         connecting_port = connecting_port or '80'
-        
+
         if serving_hostname == 'localhost':
             return True  # Safe
         elif serving_host == connecting_host:
diff --git a/flexx/event/_handler.py b/flexx/event/_handler.py
index ddac8c7..e075e2f 100644
--- a/flexx/event/_handler.py
+++ b/flexx/event/_handler.py
@@ -330,14 +330,14 @@ class Handler:
         """ Get list of events and reconnect-events from list of pending events.
         """
         events = []
-        reconnect = []
+        reconnect = {}  # poor man's set
         for label, ev in self._pending:
             if label.startswith('reconnect_'):
                 index = int(label.split('_')[-1])
-                reconnect.append(index)
+                reconnect[index] = index
             else:
                 events.append(ev)
-        return events, reconnect
+        return events, tuple(reconnect)
 
     ## Connecting
 
@@ -375,22 +375,40 @@ class Handler:
         """
         connection = self._connections[index]
 
-        # Disconnect
-        while len(connection.objects):
-            ob, type = connection.objects.pop(0)
-            ob.disconnect(type, self)
-
+        # Prepare disconnecting
+        old_objects = connection.objects  # (ob, type) tuples
+        connection.objects = []
+        
         # Obtain root object and setup connections
         ob = self._ob1()
         if ob is not None:
             self._seek_event_object(index, connection.parts, ob)
-
+        new_objects = connection.objects
+        
         # Verify
-        if not connection.objects:
+        if not new_objects:
             raise RuntimeError('Could not connect to %r' % connection.fullname)
-
-        # Connect
-        for ob, type in connection.objects:
+        
+        # Connect and disconnect
+        
+        # Skip common objects from the start
+        i1 = 0
+        while (i1 < len(new_objects) and i1 < len(old_objects) and
+               new_objects[i1][0] is old_objects[i1][0] and
+               new_objects[i1][1] == old_objects[i1][1]):
+            i1 += 1
+        # Skip common objects from the end
+        i2, i3 = len(new_objects) - 1, len(old_objects) - 1
+        while (i2 >= i1 and i3 >= i1 and
+               new_objects[i2][0] is old_objects[i3][0] and
+               new_objects[i2][1] == old_objects[i3][1]):
+            i2 -= 1
+            i3 -= 1
+        # Disconnect remaining old
+        for ob, type in old_objects[i1:i3+1]:
+            ob.disconnect(type, self)
+        # Connect remaining new
+        for ob, type in new_objects[i1:i2+1]:
             ob._register_handler(type, self, connection.force)
 
     def _seek_event_object(self, index, path, ob):
diff --git a/flexx/event/_hasevents.py b/flexx/event/_hasevents.py
index 76c6ee0..5db18ad 100644
--- a/flexx/event/_hasevents.py
+++ b/flexx/event/_hasevents.py
@@ -138,6 +138,8 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
         
         init_handlers = property_values.pop('_init_handlers', True)
         
+        self._disposed = False
+        
         # Instantiate emitters
         for name in self.__emitters__:
             self.__handlers.setdefault(name, [])
@@ -185,7 +187,8 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
     if sys.version_info > (3, 4):
         # http://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python
         def __del__(self):
-            self.dispose()
+            if not self._disposed:
+                loop.call_later(self.dispose)
     
     def dispose(self):
         """ Use this to dispose of the object to prevent memory leaks.
@@ -194,6 +197,7 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
         all references to subscribed handlers, disconnect all handlers
         defined on this object.
         """
+        self._disposed = True
         if not this_is_js():
             logger.debug('Disposing HasEvents instance %r' % self)
         for name, handlers in self.__handlers.items():


Test patch:
diff --git a/flexx/event/tests/test_handlers.py b/flexx/event/tests/test_handlers.py
index 79139fa..d52c836 100644
--- a/flexx/event/tests/test_handlers.py
+++ b/flexx/event/tests/test_handlers.py
@@ -581,4 +581,106 @@ def test_dispose3():
     assert handler_ref() is None
 
 
+def test_dynamism_and_handler_reconnecting():
+    # Flexx' event system tries to be smart about reusing connections when
+    # reconnections are made. This tests checks that this works, and when
+    # it does not.
+
+    class Foo(event.HasEvents):
+        def __init__(self):
+            super().__init__()
+        
+        @event.prop
+        def bars(self, v=[]):
+            return list(v)
+        
+        def disconnect(self, *args):  # Detect disconnections
+            super().disconnect(*args)
+            disconnects.append(self)
+    
+    class Bar(event.HasEvents):
+        def __init__(self):
+            super().__init__()
+        
+        @event.prop
+        def spam(self, v=0):
+            return v
+        
+        def disconnect(self, *args):  # Detect disconnections
+            super().disconnect(*args)
+            disconnects.append(self)
+    
+    f = Foo()
+    
+    triggers = []
+    disconnects = []
+    
+    @f.connect('!bars*.spam')
+    def handle_foo(*events):
+        triggers.append(len(events))
+    
+    assert len(triggers) == 0
+    assert len(disconnects) == 0
+    
+    # Assign new bar objects
+    with event.loop:
+        f.bars = [Bar(), Bar()]
+    #
+    assert len(triggers) == 0
+    assert len(disconnects) == 0
+    
+    # Change values of bar.spam
+    with event.loop:
+        f.bars[0].spam = 7
+        f.bars[1].spam = 42
+    #
+    assert sum(triggers) == 2
+    assert len(disconnects) == 0
+    
+    # Assign 3 new bar objects - old ones are disconnected
+    with event.loop:
+        f.bars =  [Bar(), Bar(), Bar()]
+    #
+    assert sum(triggers) == 2
+    assert len(disconnects) == 2
+    
+    #
+    
+    # Append to bars property
+    disconnects = []
+    with event.loop:
+        f.bars = f.bars + [Bar(), Bar()]
+    assert len(disconnects) == 0
+   
+    # Append to bars property, drop one
+    disconnects = []
+    with event.loop:
+        f.bars = f.bars[:-1] + [Bar(), Bar()]
+    assert len(disconnects) == 1
+    
+    # Append to bars property, drop one at the wrong end: Flexx can't optimize
+    disconnects = []
+    with event.loop:
+        f.bars = f.bars[1:] + [Bar(), Bar()]
+    assert len(disconnects) == len(f.bars) - 1
+    
+    # Prepend to bars property
+    disconnects = []
+    with event.loop:
+        f.bars = [Bar(), Bar()] + f.bars
+    assert len(disconnects) == 0
+    
+    # Prepend to bars property, drop one
+    disconnects = []
+    with event.loop:
+        f.bars = [Bar(), Bar()] + f.bars[1:]
+    assert len(disconnects) == 1
+    
+    # Prepend to bars property, drop one at the wrong end: Flexx can't optimize
+    disconnects = []
+    with event.loop:
+        f.bars = [Bar(), Bar()] + f.bars[:-1]
+    assert len(disconnects) == len(f.bars) - 1
+
+
 run_tests_if_main()
diff --git a/flexx/event/tests/test_hasevents.py b/flexx/event/tests/test_hasevents.py
index bff8bcc..8289036 100644
--- a/flexx/event/tests/test_hasevents.py
+++ b/flexx/event/tests/test_hasevents.py
@@ -370,6 +370,7 @@ def test_disposing_emitter():
     
     del foo
     gc.collect()
+    event.loop.iter()
     assert foo_ref() is None