Repository: zoofIO/flexx
Pull request #295 (2016-11-30 08:54:47)

Rename modules in flexx.app to being private
This is a refactoring of `flexx.app`, which is limited to moving code around. The main purpose is to make the modules private to keep the namespace clean and prevent users from importing stuff from these modules; `flexx.app` should contain the full public API. Also divides classes more sensible across modules and avoid circular imports.

Extras:
* [x] Allow inspecting stack of active models, similar to `_get_active_models` but public.


Patch:
diff --git a/docs/app/api.rst b/docs/app/api.rst
index 7a52c37..0fb6122 100644
--- a/docs/app/api.rst
+++ b/docs/app/api.rst
@@ -33,6 +33,8 @@ Functions related to us Model classes as apps
 
 .. autofunction:: flexx.app.get_active_model
 
+.. autofunction:: flexx.app.get_active_models
+
 
 The Model class
 ---------------
diff --git a/flexx/app/__init__.py b/flexx/app/__init__.py
index c0949da..a1939c5 100644
--- a/flexx/app/__init__.py
+++ b/flexx/app/__init__.py
@@ -207,7 +207,7 @@ Overview of classes:
 * Server: handles http requests. Uses manager to create new app
   instances or get the page for a pending session. Hosts assets by using
   the global asset store.
-* Flexx class (in clientcore.py): more or less the JS side of a session.
+* Flexx class (in _clientcore.py): more or less the JS side of a session.
 
 """
 
@@ -216,10 +216,15 @@ logger = logging.getLogger(__name__)
 del logging
 
 # flake8: noqa
-from .clientcore import serializer
-from .session import manager, Session
-from .model import Model, get_active_model
-from .model import get_instance_by_id, get_model_classes
-from .funcs import create_server, current_server, run, start, stop, call_later
-from .funcs import init_interactive, init_notebook, App, serve, launch, export
-from .assetstore import assets, Asset, Bundle, JSModule
+from ._app import App, manager
+from ._asset import Asset, Bundle
+from ._model import Model, get_active_model, get_get_active_models
+from ._model import get_instance_by_id, get_model_classes
+from ._funcs import run, start, stop
+from ._funcs import init_interactive, init_notebook, serve, launch, export
+from ._server import call_later, create_server, current_server
+from ._session import Session
+from ._modules import JSModule
+from ._assetstore import assets
+from ._clientcore import serializer
+from . import _tornadoserver  # import excplicitly to help tools like cx_Freeze
diff --git a/flexx/app/session.py b/flexx/app/_app.py
similarity index 50%
rename from flexx/app/session.py
rename to flexx/app/_app.py
index b356f87..e586e5f 100644
--- a/flexx/app/session.py
+++ b/flexx/app/_app.py
@@ -1,15 +1,225 @@
 """
-Definition of App class and the app manager.
+Definition of the App class and app manager.
 """
 
+import os
 import time
 import weakref
 
-from .. import event
-from .model import Model, new_type
-from .assetstore import SessionAssets
+from .. import event, webruntime
+
+from ._model import Model
+from ._server import current_server
+from ._session import Session
+from ._assetstore import assets
 from . import logger
 
+
+class ExporterWebSocketDummy:
+    """ Object that can be used by an app inplace of the websocket to
+    export apps to standalone HTML. The object tracks the commands send
+    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', 
+        #                                                       '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.')
+        self._cls = cls
+        self.args = args
+        self.kwargs = kwargs
+        self._path = cls.__name__  # can be overloaded by serve()
+        self._is_served = False
+    
+    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
+        been called yet or if Flexx' server is not yet running.
+        """
+        server = current_server(False)
+        if not self._is_served:
+            raise RuntimeError('Cannot determine app url if app is not yet "served".')
+        elif not (server and server.serving):
+            raise RuntimeError('Cannot determine app url if the server is not '
+                               'yet running.')
+        else:
+            host, port = server.serving
+            return 'http://%s:%i/%s/' % (host, port, self._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.
+        """
+        # Note: this talks to the manager; it has nothing to do with the server
+        if self._is_served:
+            raise RuntimeError('This app (%s) is already served.' % self.name)
+        if name is not None:
+            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 'xul'.
+            runtime_kwargs: kwargs to pass to the ``webruntime.launch`` function.
+        
+        Returns:
+            app (Model): an instance of the given class.
+        """
+        # Create session
+        if not self._is_served:
+            self.serve()
+        session = manager.create_session(self.name)
+        
+        # Launch web runtime, the server will wait for the connection
+        current_server()  # creates server if it did not yet exist
+        if runtime == 'nodejs':
+            js_assets, _ = session.get_assets_in_order()
+            all_js = '\n\n'.join([asset.to_string() for asset in js_assets])
+            session._runtime = webruntime.launch(self.url, runtime=runtime,
+                                                 code=all_js)
+        else:
+            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).
+                * 3: (default) normal assets are linked, remote assets remain remote.
+            write_shared (bool): if True (default) will also write shared assets
+                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 = session.get_page_for_export(exporter._commands, link)
+        if filename is None:
+            return html
+        elif filename.lower().endswith('.hta'):
+            hta_tag = '<meta http-equiv="x-ua-compatible" content="ie=edge" />'
+            html = html.replace('<head>', '<head>\n    ' + hta_tag, 1)
+        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))
+        if link:
+            if write_shared:
+                assets.export(os.path.dirname(filename))
+            session._export(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))
+
+
 # todo: thread safety
 
 def valid_app_name(name):
@@ -40,7 +250,6 @@ class AppManager(event.HasEvents):
         After registering an app (and starting the server) it is
         possible to connect to "http://address:port/app_name".
         """
-        from .funcs import App  # todo: put App class and manager in separate module
         assert isinstance(app, App)
         name = app.name
         if not valid_app_name(name):
@@ -228,209 +437,3 @@ class AppManager(event.HasEvents):
 
 # Create global app manager object
 manager = AppManager()
-
-
-class Session(SessionAssets):
-    """
-    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.
-    """
-    
-    STATUS = new_type('Enum', (), {'PENDING': 1, 'CONNECTED': 2, 'CLOSED': 0})
-    
-    def __init__(self, app_name):
-        super().__init__()
-        
-        self._app_name = app_name  # name of the app, available before the app itself
-        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
-        
-        # A counter to generate model id's, used by the Model class
-        self._modelcounter = 0
-        
-        # 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 = []
-        
-        # Objects that are guarded from deletion: id: (ping_count, instance)
-        self._instances_guarded = {}
-        
-        self._creation_time = time.time()
-    
-    def __repr__(self):
-        s = self.status
-        return '<Session for %r (%i) at 0x%x>' % (self.app_name, s, id(self))
-    
-    @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. Can be None if Flexx
-        is used in interactive mode (using the ``__default__`` 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
-    
-    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
-        the session_id so it can be connected to the correct Session
-        via this method
-        """
-        if self._ws is not None:
-            raise RuntimeError('Session is already connected.')
-        # Set websocket object - this is what changes the status to CONNECTED
-        self._ws = ws  
-        # todo: make icon and title work again. Also in exported docs.
-        # Set some app specifics
-        # self._ws.command('ICON %s.ico' % self.id)
-        # self._ws.command('TITLE %s' % self._config.title)
-        # Send pending commands
-        for command in self._pending_commands:
-            self._ws.command(command)
-        self._ws.command('INIT-DONE')
-   
-    def _set_app(self, model):
-        if self._model is not None:
-            raise RuntimeError('Session already has an associated Model.')
-        self._model = model
-        # todo: connect to title change and icon change events
-    
-    def _set_runtime(self, runtime):
-        if self._runtime is not None:
-            raise RuntimeError('Session already has a runtime.')
-        self._runtime = runtime
-    
-    def close(self):
-        """ Close the session: close websocket, close runtime, dispose app.
-        """
-        # Stop guarding objects to break down any circular refs
-        for id in list(self._instances_guarded.keys()):
-            self._instances_guarded.pop(id)
-        self._closing = True  # suppress warnings for session being closed.
-        try:
-            
-            # Close the websocket
-            if self._ws:
-                self._ws.close_this()
-            # Close the runtime
-            if self._runtime:
-                self._runtime.close()
-            # Dispose the model and break the circular reference
-            if self._model:
-                self._model.dispose()
-                self._model = None
-        finally:
-            self._closing = False
-    
-    @property
-    def status(self):
-        """ The status of this session. The lifecycle for each session is:
-        
-        * status 1: pending
-        * statys 2: connected
-        * status 0: closed
-        """
-        if self._ws is None:
-            return self.STATUS.PENDING  # not connected yet
-        elif self._ws.close_code is None:
-            return self.STATUS.CONNECTED  # alive and kicking
-        else:
-            return self.STATUS.CLOSED  # connection closed
-    
-    def _send_command(self, command):
-        """ Send the command, add to pending queue.
-        """
-        if self._closing:
-            pass
-        elif self.status == self.STATUS.CONNECTED:
-            self._ws.command(command)
-        elif self.status == self.STATUS.PENDING:
-            self._pending_commands.append(command)
-        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.
-        """
-        if command.startswith('RET '):
-            print(command[4:])  # Return value
-        elif command.startswith('ERROR '):
-            logger.error('JS - ' + command[6:].strip() +
-                         ' (stack trace in browser console)')
-        elif command.startswith('WARN '):
-            logger.warn('JS - ' + command[5:].strip())
-        elif command.startswith('PRINT '):
-            print(command[5:].strip())
-        elif command.startswith('INFO '):
-            logger.info('JS - ' + command[5:].strip())
-        elif command.startswith('SET_PROP '):
-            # todo: seems weird to deal with here. implement by registring some handler?
-            # Should be better when we implement a more formal protocol
-            _, id, name, txt = command.split(' ', 3)
-            ob = Model._instances.get(id, None)
-            if ob is not None:
-                ob._set_prop_from_js(name, txt)
-        elif command.startswith('SET_EVENT_TYPES '):
-            _, id, txt = command.split(' ', 3)
-            ob = Model._instances.get(id, None)
-            if ob is not None:
-                ob._set_event_types_js(txt)
-        elif command.startswith('EVENT '):
-            _, id, name, txt = command.split(' ', 3)
-            ob = Model._instances.get(id, None)
-            if ob is not None:
-                ob._emit_from_js(name, txt)
-        else:
-            logger.warn('Unknown command received from JS:\n%s' % command)
-    
-    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
-        objects survive jitter due to synchronisation, though any type
-        of object can be given.
-        """
-        obid = id(ob)
-        counter = 0 if self._ws is None else self._ws.ping_counter
-        lifetime = counter + int(iters)
-        if lifetime > self._instances_guarded.get(obid, (0, ))[0]:
-            self._instances_guarded[obid] = lifetime, ob
-    
-    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
-        "timeout counter" has expired.
-        """
-        objects_to_clear = [ob for c, ob in
-                           self._instances_guarded.values() if c <= count]
-        for ob in objects_to_clear:
-            self._instances_guarded.pop(id(ob))
-    
-    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.
-        """
-        if self._ws is None:
-            raise RuntimeError('App not connected')
-        self._send_command('EVAL ' + code)
diff --git a/flexx/app/asset.py b/flexx/app/_asset.py
similarity index 99%
rename from flexx/app/asset.py
rename to flexx/app/_asset.py
index 19d4a52..ef0ee66 100644
--- a/flexx/app/asset.py
+++ b/flexx/app/_asset.py
@@ -247,7 +247,7 @@ class Bundle(Asset):
         deps = set()
         module_deps = m.deps
         if ext == '.js':
-            module_deps.add('flexx.app.clientcore')
+            module_deps.add('flexx.app._clientcore')
         for dep in module_deps:
             while '.' in dep:
                 deps.add(dep)
diff --git a/flexx/app/assetstore.py b/flexx/app/_assetstore.py
similarity index 50%
rename from flexx/app/assetstore.py
rename to flexx/app/_assetstore.py
index c9d165a..7384c1d 100644
--- a/flexx/app/assetstore.py
+++ b/flexx/app/_assetstore.py
@@ -1,23 +1,18 @@
 """
-Flexx asset and data management system. The purpose of these classes
+Flexx asset and data management system. The purpose of this class
 is to provide the assets (JavaScript and CSS files) and data (images,
 etc.) needed by the applications.
 """
 
 import os
-import json
-import time
-import random
 import shutil
-import hashlib
 from urllib.request import urlopen
 
 from ..pyscript import create_js_module, get_all_std_names, get_full_std_lib
-from .. import config
 
-from .model import Model
-from .modules import JSModule
-from .asset import Asset, Bundle, solve_dependencies, HEADER
+from ._model import Model
+from ._asset import Asset, Bundle, HEADER
+from ._modules import JSModule
 from . import logger
 
 
@@ -167,31 +162,6 @@ table{border-collapse:collapse;border-spacing:0}
 td,th{padding:0}
 """.lstrip()
 
-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. 
-
-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
-    """
-    allowed_chars = allowed_chars or ('abcdefghijklmnopqrstuvwxyz' +
-                                      'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
-    try:
-        srandom = random.SystemRandom()
-    except NotImplementedError:  # pragma: no cover
-        srandom = random
-        logger.warn('Falling back to less secure Mersenne Twister random string.')
-        bogus = "%s%s%s" % (random.getstate(), time.time(), 'sdkhfbsdkfbsdbhf')
-        random.seed(hashlib.sha256(bogus.encode()).digest())
-
-    return ''.join(srandom.choice(allowed_chars) for i in range(length))
-
 
 def export_assets_and_data(assets, data, dirname, app_id, clear=False):
     """ Export the given assets (list of Asset objects) and data (list of
@@ -478,352 +448,3 @@ class AssetStore:
 
 # Our singleton asset store
 assets = AssetStore()
-
-
-class SessionAssets:
-    """ Provider for assets of a specific session. Inherited by Session.
-    
-    The responsibility of this class is to keep track of what JSModules
-    are being used, to provide the associated bundles and assets, and to
-    dynamically define assets when needed. Further this class takes
-    care of per-session data.
-    """
-    
-    def __init__(self, store=None):  # Allow custom store for testing
-        self._store = store if (store is not None) else assets
-        assert isinstance(self._store, AssetStore)
-        
-        self._id = get_random_string()
-        self._app_name = ''
-        
-        # Keep track of all assets for this session. Assets that are provided
-        # by the asset store have a value of None.
-        self._used_classes = set()  # Model classes registered as used
-        self._used_modules = set()  # module names that define used classes, plus deps
-        self._loaded_modules = set()  # module names that were present in bundles
-        # Data for this session (in addition to the data provided by the store)
-        # todo: get rid of session assets alltogether, or is there a use-case?
-        self._assets = {}
-        self._data = {}
-        # Whether the page has been served already
-        self._served = 0
-        self._is_interactive = None
-    
-    @property
-    def id(self):
-        """ The unique identifier of this session.
-        """
-        return self._id
-    
-    def get_data_names(self):
-        """ Get a list of names of the data provided by this session, in
-        the order that they were added.
-        """
-        return list(self._data.keys())  # Note: order matters
-    
-    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
-        by that name is unknown.
-        """
-        data = self._data.get(name, None)
-        if data is None:
-            data = self._store.get_data(name)
-        return data
-    
-    # todo: the way that we do assets now makes me wonder whether there are better ways
-    # to deal with data handling ...
-    
-    def add_data(self, name, data):  # todo: add option to clear data after its loaded?
-        """ 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 ``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. Can also be a uri to the blob
-                (string starting with "file://", "http://" or "https://").
-        """
-        if not isinstance(name, str):
-            raise TypeError('Session.add_data() name must be a str.')
-        if name in self._data:
-            raise ValueError('Session.add_data() got existing name %r.' % name)
-        if isinstance(data, str):
-            if data.startswith('file://'):
-                data = open(data.split('//', 1)[1], 'rb').read()
-            elif data.startswith(('http://', 'https://')):
-                data = urlopen(data, timeout=5.0).read()
-        if not isinstance(data, bytes):
-            raise TypeError('Session.add_data() data must be a 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.
-        """
-        self._data.pop(name, None)
-    
-    def register_model_class(self, cls):
-        """ Mark the given Model class as used; ensure that the client
-        knows about it.
-        """
-        if not (isinstance(cls, type) and issubclass(cls, Model)):
-            raise ValueError('Not a Model class')
-            
-        # Early exit if we know the class already
-        if cls in self._used_classes:
-            return
-        
-        # Make sure the base classes are registered first
-        for cls2 in cls.mro()[1:]:
-            if not issubclass(cls2, Model):  # True if cls2 is *the* Model class
-                break
-            if cls2 not in self._used_classes:
-                self.register_model_class(cls2)
-        
-        # Ensure interactive flag - e.g. for in the notebook
-        if self._is_interactive is None:
-            from .session import manager  # noqa - avoid circular import
-            self._is_interactive = self is manager.get_default_session()
-        
-        # Make sure that no two models have the same name, or we get problems
-        # that are difficult to debug. Unless classes are defined in the notebook.
-        same_name = [c for c in self._used_classes if c.__name__ == cls.__name__]
-        if same_name:
-            same_name.append(cls)
-            is_dynamic_cls = all([c.__module__ == '__main__' for c in same_name])
-            if not (self._is_interactive and is_dynamic_cls):
-                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._used_classes.add(cls)
-        self._store.update_modules(cls)  # Update module definition
-        self._register_module(cls.__jsmodule__)
-    
-    def _register_module(self, mod_name):
-        """ Mark a module (and its dependencies) as used. If the page is
-        already served, will inject the module dynamically.
-        """
-        
-        if not self._served:
-            # Not served yet, register asset as used so we can serve it later
-            if mod_name not in self._used_modules:
-                self._used_modules.add(mod_name)
-                mod = self._store.modules[mod_name]
-                for dep in mod.deps:
-                    self._register_module(dep)
-        
-        else:
-            # Already served, we might need to load dynamically. We simply
-            # check whether a module is new or has changed since its source
-            # was last obtained. E.g. it could be that its a new class for 
-            # this session, but that it was loaded as part of the bundle.
-            mod = self._store.modules[mod_name]
-            modules = [m for m in self._store.modules.values()
-                       if m.name not in self._loaded_modules or
-                       m.changed_time >= self._served]
-            modules = solve_dependencies(modules)  # sort based on deps
-            if modules:
-                # Bundles - the dash makes this bundle have an empty "module name"
-                js_asset = Bundle('-extra.js')
-                css_asset = Bundle('-extra.css')
-                for mod in modules:
-                    js_asset.add_module(mod)
-                    css_asset.add_module(mod)
-                # Load assets of modules that were not yet used
-                for mod in modules:
-                    if mod.name not in self._used_modules:
-                        for asset in self._store.get_associated_assets(mod.name):
-                            self._inject_asset_dynamically(asset)
-                # Load bundles
-                self._inject_asset_dynamically(css_asset)
-                self._inject_asset_dynamically(js_asset)
-                # Mark the modules as used and loaded
-                for mod in modules:
-                    self._used_modules.add(mod.name)
-    
-    def _inject_asset_dynamically(self, asset):
-        """ Load an asset in a running session.
-        This method assumes that this is a Session class.
-        """
-        logger.debug('Dynamically loading asset %r' % asset.name)
-        
-        in_notebook = (self._is_interactive and
-                       getattr(self, 'init_notebook_done', False))
-        
-        if in_notebook:
-            # Load using IPython constructs
-            from IPython.display import display, HTML
-            if asset.name.lower().endswith('.js'):
-                display(HTML("<script>%s</script>" % asset.to_string()))
-            else:
-                display(HTML("<style>%s</style>" % asset.to_string()))
-        else:
-            # Load using Flexx construct (using Session._send_command())
-            suffix = asset.name.split('.')[-1].upper()
-            self._send_command('DEFINE-%s %s' % (suffix, asset.to_string()))
-    
-    def get_assets_in_order(self, css_reset=False, load_all=None, bundle_level=None):
-        """ Get two lists containing the JS assets and CSS assets,
-        respectively. The assets contain bundles corresponding to all modules
-        being used (and their dependencies). The order of bundles is based on
-        the dependency resolution. The order of other assets is based on the
-        order in which assets were instantiated. Special assets are added, such
-        as the CSS reset and the JS module loader.
-        
-        After this function gets called, it is assumed that the assets have
-        been served and that future asset loads should be done dynamically.
-        """
-        
-        # Make store aware of everything that we know now
-        self._store.update_modules()
-        
-        if load_all is None:
-            load_all = config.bundle_all
-        if load_all:
-            modules_to_load = self._store.modules.keys()  # e.g. notebook
-        else:
-            modules_to_load = self._used_modules
-        
-        # Get bundle names that contain all the used modules. We use
-        # bundledversions, which means that we load more modules than
-        # we use. In this step we can make a lot of choices with regard
-        # to how much modules we want to pack in a bundle. We could use
-        # a different depth per branch, we could create session-specific
-        # bundles, we could allow users to define a bundle, etc. For
-        # now, we just truncate at a certain level.
-        # todo: this could be configurable, e.g. 99 for dev, 1 for prod
-        level = max(1, bundle_level or 2)
-        bundle_names = set()
-        for mod_name in modules_to_load:
-            bundle_names.add('.'.join(mod_name.split('.')[:level]))
-        
-        # Get bundles
-        js_assets = [self._store.get_asset(b + '.js') for b in bundle_names]
-        css_assets = [self._store.get_asset(b + '.css') for b in bundle_names]
-        
-        # Get loaded modules
-        for asset in js_assets:
-            self._loaded_modules.update([m.name for m in asset.modules])
-        
-        # Sort bundles by name and dependency resolution
-        f = lambda m: (m.name.startswith('__main__'), m.name)
-        js_assets = solve_dependencies(sorted(js_assets, key=f))
-        css_assets = solve_dependencies(sorted(css_assets, key=f))
-        
-        # Filter out empty css bundles
-        css_assets = [asset for asset in css_assets
-                      if any([m.get_css().strip() for m in asset.modules])]
-        
-        # Collect non-module assets
-        # Assets only get included if they are in a module that is *used*.
-        asset_deps_before = set()
-        # asset_deps_after = set()
-        for mod_name in self._used_modules:
-            asset_deps_before.update(self._store.get_associated_assets(mod_name))
-        
-        # Push assets in the lists (sorted by the creation time)
-        f = lambda a: a.i
-        for asset in reversed(sorted(asset_deps_before, key=f)):
-            if asset.name.lower().endswith('.js'):
-                js_assets.insert(0, asset)
-            else:
-                css_assets.insert(0, asset)
-       
-        # Mark all assets as used. For now, we only use assets that are available
-        # in the asset store.
-        for asset in js_assets + css_assets:
-            self._assets[asset.name] = None
-        
-        
-        # Prepend reset.css
-        if css_reset:
-            css_assets.insert(0, self._store.get_asset('reset.css'))
-        
-        # Prepend flexx-info, module loader, and pyscript std
-        js_assets.insert(0, self._store.get_asset('pyscript-std.js'))
-        js_assets.insert(0, self._store.get_asset('flexx-loader.js'))
-        t = 'var flexx = {app_name: "%s", session_id: "%s"};'
-        js_assets.insert(0, Asset('flexx-info.js', t % (self._app_name, self.id)))
-        
-        # Mark this session as served; all future asset loads are dynamic
-        self._served = time.time()
-        
-        # todo: fix incorrect order; loader should be able to handle it for JS
-        #import random
-        #random.shuffle(js_assets)
-        
-        return js_assets, css_assets
-    
-    def get_page(self, link=3):
-        """ Get the string for the HTML page to render this session's app.
-        """
-        js_assets, css_assets = self.get_assets_in_order(True)
-        for asset in js_assets + css_assets:
-            if asset.remote and asset.source.startswith('file://'):
-                raise RuntimeError('Can only use remote assets with "file://" '
-                                   'when exporting.')
-        return self._get_page(js_assets, css_assets, link, False)
-    
-    def get_page_for_export(self, commands, link=0):
-        """ Get the string for an exported HTML page (to run without a server).
-        """
-        # Create lines to init app
-        lines = []
-        lines.append('flexx.is_exported = true;\n')
-        lines.append('flexx.runExportedApp = function () {')
-        lines.extend(['    flexx.command(%s);' % reprs(c) for c in commands])
-        lines.append('};\n')
-        # Create a session asset for it, "-export.js" is always embedded
-        export_asset = Asset('flexx-export.js', '\n'.join(lines))
-        # Compose
-        bundle_level = 2 if (link >= 2) else 9
-        js_assets, css_assets = self.get_assets_in_order(css_reset=True,
-                                                         bundle_level=bundle_level)
-        js_assets.append(export_asset)
-        return self._get_page(js_assets, css_assets, link, True)
-    
-    def _get_page(self, js_assets, css_assets, link, export):
-        """ Compose index page.
-        """
-        pre_path = '_assets' if export else '/flexx/assets'
-        
-        codes = []
-        for assets in [css_assets, js_assets]:
-            for asset in assets:
-                if not link:
-                    html = asset.to_html('{}', link)
-                else:
-                    if asset.name.endswith(('-info.js', '-export.js')):
-                        html = asset.to_html('', 0)
-                    elif self._store.get_asset(asset.name) is not asset:
-                        html = asset.to_html(pre_path + '/%s/{}' % self.id, link)
-                    else:
-                        html = asset.to_html(pre_path + '/shared/{}', link)
-                codes.append(html)
-            codes.append('')  # whitespace between css and js assets
-        
-        src = INDEX
-        if not link:
-            asset_names = [a.name for a in css_assets + js_assets]
-            toc = '<!-- Contents:\n\n- ' + '\n- '.join(asset_names) + '\n\n-->'
-            codes.insert(0, toc)
-            src = src.replace('ASSET-HOOK', '\n\n\n'.join(codes))
-        else:
-            src = src.replace('ASSET-HOOK', '\n'.join(codes))
-        
-        return src
-    
-    def _export(self, dirname, clear=False):
-        """ Export all assets and data specific to this session.
-        Private method, used by app.export().
-        """
-        # Note that self.id will have been set to the app name.
-        assets = []
-        data = [(name, self.get_data(name)) for name in self.get_data_names()]
-        export_assets_and_data(assets, data, dirname, self.id, clear)
-        logger.info('Exported assets and data for %r to %r.' % (self.id, dirname))
diff --git a/flexx/app/clientcore.py b/flexx/app/_clientcore.py
similarity index 100%
rename from flexx/app/clientcore.py
rename to flexx/app/_clientcore.py
diff --git a/flexx/app/_funcs.py b/flexx/app/_funcs.py
new file mode 100644
index 0000000..9b4dd73
--- /dev/null
+++ b/flexx/app/_funcs.py
@@ -0,0 +1,274 @@
+"""
+Functional API for flexx.app
+"""
+
+import sys
+import json
+
+from .. import webruntime, config, set_log_level
+
+from ._app import App, manager
+from ._model import Model
+from ._server import current_server
+from . import logger
+
+reprs = json.dumps
+
+## Main loop functions
+
+
+def start():
+    """
+    Start the server and event loop. This function generally does not
+    return until the application is stopped (although it may in
+    interactive environments (e.g. Pyzo)).
+    """
+    server = current_server()
+    logger.info('Starting Flexx event loop.')
+    server.start()
+
+
+def run():
+    """
+    Start the event loop in desktop app mode; the server will close
+    down when there are no more connections.
+    """
+    server = current_server()
+    server._auto_stop = True
+    return start()
+
+
+def stop():
+    """
+    Stop the event loop. This function is thread safe (it can be used
+    even if ``flexx.start()`` was called from another thread). 
+    The server can be restarted after it has been stopped. Note that
+    calling ``stop()`` too often will cause a subsequent call to `start()``
+    to return almost immediately.
+    """
+    server = current_server()
+    server.stop()
+
+
+@manager.connect('connections_changed')
+def _auto_closer(*events):
+    server = current_server()
+    if not getattr(server, '_auto_stop', False):
+        return
+    for name in manager.get_app_names():
+        proxies = manager.get_connections(name)
+        if proxies:
+            return
+    else:
+        logger.info('Stopping Flexx event loop.')
+        server.stop()
+
+
+## App functions
+
+
+def init_interactive(cls=None, runtime=None):
+    """ Initialize Flexx for interactive mode. This creates a default session
+    and launches a runtime to connect to it. 
+    
+    Parameters:
+        cls (None, Model): a subclass of ``app.Model`` (or ``ui.Widget``) to use
+            as the *default active model*. Only has effect the first time that
+            this function is called.
+        runtime (str): the runtime to launch the application in. Default 'xul'.
+    """
+    
+    # Determine default model class (which is a Widget if ui is imported)
+    if cls is None and 'flexx.ui' in sys.modules:
+        from .. import ui
+        cls = ui.Widget
+    
+    # Create the default session
+    session = manager.get_default_session()
+    if session is None:
+        session = manager.create_default_session(cls)
+    else:
+        return  # default session already running
+
+    # Launch web runtime, the server will wait for the connection
+    server = current_server()
+    host, port = server.serving
+    url = '%s:%i/%s/?session_id=%s' % (host, port, session.app_name, session.id)
+    session._runtime = launch('http://' + url, runtime=runtime)
+    
+
+class NoteBookHelper:
+    """ Object that captures commands send to the websocket during the
+    execution of a cell, and then applies these commands using a script
+    node. This way, Flexx widgets keep working in the exported notebook.
+    """
+    
+    close_code = None
+    
+    def __init__(self, session):
+        self._session = session
+        self._real_ws = None
+        self._commands = []
+        self.enable()
+    
+    def enable(self):
+        from IPython import get_ipython
+        ip = get_ipython()
+        ip.events.register('pre_execute', self.capture)
+        ip.events.register('post_execute', self.release)
+    
+    def capture(self):
+        if self._real_ws is not None:
+            logger.warn('Notebookhelper already is in capture mode.')
+        else:
+            assert self._session._ws is not None
+            self._real_ws = self._session._ws
+            self._session._ws = self
+    
+    def release(self):
+        if self._session._ws is self:
+            self._session._ws = self._real_ws
+        self._real_ws = None
+        if self._commands:
+            from IPython.display import display, Javascript
+            commands = ['flexx.command(%s);' % reprs(msg) for msg in self._commands]
+            self._commands = []
+            display(Javascript('\n'.join(commands)))
+    
+    def command(self, msg):
+        self._commands.append(msg)
+    
+    @property
+    def ping_counter(self):
+        if self._session._ws is self:
+            return self._real_ws.ping_counter
+        else:
+            return self._session._ws.ping_counter
+
+
+def init_notebook():
+    """ Initialize the Jupyter notebook by injecting the necessary CSS
+    and JS into the browser. Note that any Flexx-based libraries that
+    you plan to use should probably be imported *before* calling this.
+    """
+    
+    # Note: not using IPython Comm objects yet, since they seem rather
+    # undocumented and I could not get them to work when I tried for a bit.
+    # This means though, that flexx in the notebook only works on localhost.
+    
+    from IPython.display import display, clear_output, HTML
+    # from .. import ui  # noqa - make ui assets available
+    
+    # Make default log level warning instead of "info" to avoid spamming
+    # This preserves the log level set by the user
+    config.load_from_string('log_level = warning', 'init_notebook')
+    set_log_level(config.log_level)
+    
+    # Get session or create new
+    session = manager.get_default_session()
+    if session is None:
+        session = manager.create_default_session()
+    
+    # Open server - the notebook helper takes care of the JS resulting
+    # from running a cell, but any interaction goes over the websocket.
+    server = current_server()
+    host, port = server.serving
+    
+    # Trigger loading phosphor assets
+    if 'flexx.ui' in sys.modules:
+        from flexx import ui
+        session.register_model_class(ui.Widget)
+    
+    # Get assets, load all known modules to prevent dynamic loading as much as possible
+    js_assets, css_assets = session.get_assets_in_order(css_reset=False, load_all=True)
+    
+    # Pop the first JS asset that sets flexx.app_name and flexx.session_id
+    # We set these in a way that it does not end up in exported notebook.
+    js_assets.pop(0)
+    url = 'ws://%s:%i/flexx/ws/%s' % (host, port, session.app_name)
+    flexx_pre_init = """<script>window.flexx = window.flexx || {};
+                                window.flexx.app_name = "%s";
+                                window.flexx.session_id = "%s";
+                                window.flexx.ws_url = "%s";
+                                window.flexx.is_live_notebook = true;
+                        </script>""" % (session.app_name, session.id, url)
+    
+    # Check if already loaded, if so, re-connect
+    if not getattr(session, 'init_notebook_done', False):
+        session.init_notebook_done = True  # also used in assetstore
+    else:
+        display(HTML(flexx_pre_init))
+        clear_output()
+        display(HTML("""<script>
+                        flexx.is_exported = !flexx.is_live_notebook;
+                        flexx.init();
+                        </script>
+                        <i>Flexx already loaded. Reconnected.</i>
+                        """))
+        return  # Don't inject Flexx twice
+        # Note that exporting will not work anymore since out assets
+        # are no longer in the outputs
+    
+    # Install helper to make things work in exported notebooks
+    NoteBookHelper(session)
+    
+    # Compose HTML to inject
+    t = "<i>Injecting Flexx JS and CSS</i>"
+    t += '\n\n'.join([asset.to_html('{}', 0) for asset in css_assets + js_assets])
+    t += """<script>
+            flexx.is_notebook = true;
+            flexx.is_exported = !flexx.is_live_notebook;
+            /* If Phosphor is already loaded, disable our Phosphor CSS. */
+            if (window.jupyter && window.jupyter.lab) {
+                document.getElementById('phosphor-all.css').disabled = true;
+            }
+            flexx.init();
+            </script>"""
+    
+    display(HTML(flexx_pre_init))  # Create initial Flexx info dict
+    clear_output()  # Make sure the info dict is gone in exported notebooks
+    display(HTML(t))
+    
+    # Note: the Widget._repr_html_() method is responsible for making
+    # the widget show up in the notebook output area.
+
+
+# todo: deprecate these
+
+def serve(cls, name=None, properties=None):
+    """ Backwards compat.
+    """
+    # Note: this talks to the manager; it has nothing to do with the server
+    assert (isinstance(cls, type) and issubclass(cls, Model))
+    a = App(cls, **(properties or {}))
+    a.serve(name)
+    return cls
+
+
+def launch(cls, runtime=None, properties=None, **runtime_kwargs):
+    """ Backwards compat.
+    """
+    if isinstance(cls, str):
+        return webruntime.launch(cls, runtime, **runtime_kwargs)
+    assert (isinstance(cls, type) and issubclass(cls, Model))
+    a = App(cls, **(properties or {}))
+    return a.launch(runtime, **runtime_kwargs)
+
+
+def export(cls, filename=None, properties=None, single=None, link=None,
+           write_shared=True):
+    """ Backward compat.
+    """
+    if not (isinstance(cls, type) and issubclass(cls, Model)):
+        raise ValueError('runtime must be a string or Model subclass.')
+    
+    # Backward comp - deprecate "single" argument at some point
+    if link is None:
+        if single is not None:
+            logger.warn('Export single arg is deprecated, use link instead.')
+            if not single:
+                link = 3
+    link = int(link or 0)
+    
+    a = App(cls, **(properties or {}))
+    return a.export(filename, link=link, write_shared=write_shared)
diff --git a/flexx/app/model.py b/flexx/app/_model.py
similarity index 98%
rename from flexx/app/model.py
rename to flexx/app/_model.py
index ce0306a..567179d 100644
--- a/flexx/app/model.py
+++ b/flexx/app/_model.py
@@ -77,7 +77,8 @@ from ..event._emitters import Emitter
 from ..event._js import create_js_hasevents_class, HasEventsJS
 from ..pyscript import js_rename, window, JSString
 
-from .asset import get_mod_name
+from ._asset import get_mod_name
+from ._server import call_later
 from . import logger
 
 # The clientcore module is a PyScript module that forms the core of the
@@ -86,13 +87,11 @@ from . import logger
 # corresponding instance from the module that's being used.
 # By using something from clientcore in JS here, we make clientcore a
 # dependency of the the current module.
-from .clientcore import serializer
+from ._clientcore import serializer
 
 
 reprs = json.dumps
 
-call_later = None  # reset in func.py to deal with circular dependency
-
 
 def get_model_classes():
     """ Get a list of all known Model subclasses.
@@ -119,8 +118,8 @@ _active_models_per_thread = {}  # dict of threadid -> list
 
 def _get_active_models():
     """ Get list that represents the stack of "active" models.
-    Each thread has its own stack. Should only be used inside a Model
-    context manager.
+    Each thread has its own stack. Should only be used directly inside
+    a Model context manager.
     """
     # Get thread id
     if hasattr(threading, 'current_thread'):
@@ -131,6 +130,13 @@ def _get_active_models():
     return _active_models_per_thread.setdefault(tid, [])
 
 
+def get_get_active_models():
+    """ Get a tuple of Model instance that represent the stack of "active"
+    models. Each thread has its own stack. Also see get_active_model().
+    """
+    return tuple(_get_active_models())
+
+
 def get_active_model():
     """ If the execution is now in a Model context manager, return the
     corresponding object, and None otherwise. Can be used by subclasses
@@ -140,7 +146,7 @@ def get_active_model():
     if models:
         return models[-1]
     else:
-        from .session import manager
+        from ._app import manager  # noqa - circular dependency
         session = manager.get_default_session()
         if session is not None:
             return session.app
diff --git a/flexx/app/modules.py b/flexx/app/_modules.py
similarity index 99%
rename from flexx/app/modules.py
rename to flexx/app/_modules.py
index 499f0bf..260d7d7 100644
--- a/flexx/app/modules.py
+++ b/flexx/app/_modules.py
@@ -11,8 +11,8 @@ import types
 
 from ..pyscript import py2js, RawJS, JSConstant, create_js_module, get_all_std_names
 
-from .model import Model
-from .asset import Asset, get_mod_name, module_is_package
+from ._model import Model
+from ._asset import Asset, get_mod_name, module_is_package
 from . import logger
 
 
diff --git a/flexx/app/_server.py b/flexx/app/_server.py
new file mode 100644
index 0000000..79702e9
--- /dev/null
+++ b/flexx/app/_server.py
@@ -0,0 +1,176 @@
+"""
+High level code related to the server that provides a mainloop and
+serves the pages and websocket. Also provides call_later().
+"""
+
+from ..event import _loop
+from .. import config
+
+
+# There is always a single current server (except initially there is None)
+_current_server = None
+
+
+def create_server(host=None, port=None, new_loop=False, backend='tornado'):
+    """
+    Create a new server object. This is automatically called; users generally
+    don't need this, unless they want to explicitly specify host/port,
+    create a fresh server in testing scenarios, or run Flexx in a thread.
+    
+    Flexx uses a notion of a single current server object. This function
+    (re)creates that object. If there already was a server object, it is
+    replaced. It is an error to call this function if the current server
+    is still running.
+    
+    Arguments:
+        host (str): The hostname to serve on. By default
+            ``flexx.config.hostname`` is used. If ``False``, do not listen
+            (e.g. when integrating with an existing Tornado application).
+        port (int, str): The port number. If a string is given, it is
+            hashed to an ephemeral port number. By default
+            ``flexx.config.port`` is used.
+        new_loop (bool): Whether to create a fresh Tornado IOLoop instance,
+            which is made current when ``start()`` is called. If ``False``
+            (default) will use the current IOLoop for this thread.
+        backend (str): Stub argument; only Tornado is currently supported.
+    
+    Returns:
+        server: The server object, see ``current_server()``.
+    """
+    # Lazy load tornado, so that we can use anything we want there without
+    # preventing other parts of flexx.app from using *this* module.
+    from ._tornadoserver import TornadoServer  # noqa - circular dependency
+    
+    global _current_server
+    if backend.lower() != 'tornado':
+        raise RuntimeError('Flexx server can only run on Tornado (for now).')
+    # Handle defaults
+    if host is None:
+        host = config.hostname
+    if port is None:
+        port = config.port
+    # Stop old server
+    if _current_server:
+        _current_server.close()
+    # Start hosting
+    _current_server = TornadoServer(host, port, new_loop)
+    assert isinstance(_current_server, AbstractServer)
+    # Schedule pending calls
+    _current_server.call_later(0, _loop.loop.iter)
+    while _pending_call_laters:
+        delay, callback, args, kwargs = _pending_call_laters.pop(0)
+        call_later(delay, callback, *args, **kwargs)
+    return _current_server
+
+
+def current_server(create=True):
+    """
+    Get the current server object. Creates a server if there is none
+    and the ``create`` arg is True. Currently, this is always a
+    TornadoServer object, which has properties:
+    
+    * serving: a tuple ``(hostname, port)`` specifying the location
+      being served (or ``None`` if the server is closed).
+    * app: the ``tornado.web.Application`` instance
+    * loop: the ``tornado.ioloop.IOLoop`` instance
+    * server: the ``tornado.httpserver.HttpServer`` instance
+    """
+    if create and not _current_server:
+        create_server()
+    return _current_server
+
+
+def call_later(delay, callback, *args, **kwargs):
+    """
+    Schedule a function call in the current event loop. This function is
+    thread safe.
+    
+    Arguments:
+        delay (float): the delay in seconds. If zero, the callback will
+            be executed in the next event loop iteration.
+        callback (callable): the function to call.
+        args: the positional arguments to call the callback with.
+        kwargs: the keyword arguments to call the callback with.
+    """
+    server = current_server(False)
+    if not server:
+        _pending_call_laters.append((delay, callback, args, kwargs))
+    else:
+        server.call_later(delay, callback, *args, **kwargs)
+
+_pending_call_laters = []
+
+# Integrate the "event-loop" of flexx.event
+_loop.loop.integrate(lambda f: call_later(0, f))
+
+
+## Server class
+
+
+class AbstractServer:
+    """ This is an attempt to generalize the server, so that in the
+    future we may have e.g. a Flask or Pyramid server.
+    
+    A server must implement this, and use the manager to instantiate,
+    connect and disconnect sessions. The assets object must be used to
+    server assets to the client.
+    
+    Arguments:
+        host (str): the hostname to serve at
+        port (int): the port to serve at. None or 0 mean to autoselect a port.
+    """
+    
+    def __init__(self, host, port):
+        self._serving = None
+        if host is not False:
+            self._open(host, port)
+            assert self._serving  # Check that subclass set private variable
+        self._running = False
+    
+    def start(self):
+        """ Start the event loop. """
+        if not self._serving:
+            raise RuntimeError('Cannot start a closed or non-serving server!')
+        if self._running:
+            raise RuntimeError('Cannot start a running server.')
+        self._running = True
+        try:
+            self._start()
+        finally:
+            self._running = False
+    
+    def stop(self):
+        """ Stop the event loop. This does not close the connection; the server
+        can be restarted. Thread safe. """
+        self.call_later(0, self._stop)
+    
+    def close(self):
+        """ Close the connection. A closed server cannot be used again. """
+        if self._running:
+            raise RuntimeError('Cannot close a running server; need to stop first.')
+        self._serving = None
+        self._close()
+    
+    def _open(self, host, port):
+        raise NotImplementedError()
+    
+    def _start(self):
+        raise NotImplementedError()
+    
+    def _stop(self):
+        raise NotImplementedError()
+    
+    def _close(self):
+        raise NotImplementedError()
+    
+    # This method must be implemented directly for performance (its used a lot)
+    def call_later(self, delay, callback, *args, **kwargs):
+        """ Call a function in a later event loop iteration. """
+        raise NotImplementedError()
+    
+    @property
+    def serving(self):
+        """ Get a tuple (hostname, port) that is being served.
+        Or None if the server is not serving (anymore).
+        """
+        return self._serving
diff --git a/flexx/app/_session.py b/flexx/app/_session.py
new file mode 100644
index 0000000..863cda6
--- /dev/null
+++ b/flexx/app/_session.py
@@ -0,0 +1,596 @@
+"""
+Definition of the Session class.
+"""
+
+import time
+import json
+import random
+import hashlib
+from urllib.request import urlopen
+
+from .. import config
+
+from ._model import Model, new_type
+from ._asset import Asset, Bundle, solve_dependencies
+from ._assetstore import AssetStore, export_assets_and_data, INDEX
+from ._assetstore import assets as assetstore
+from . import logger
+
+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. 
+
+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
+    """
+    allowed_chars = allowed_chars or ('abcdefghijklmnopqrstuvwxyz' +
+                                      'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
+    try:
+        srandom = random.SystemRandom()
+    except NotImplementedError:  # pragma: no cover
+        srandom = random
+        logger.warn('Falling back to less secure Mersenne Twister random string.')
+        bogus = "%s%s%s" % (random.getstate(), time.time(), 'sdkhfbsdkfbsdbhf')
+        random.seed(hashlib.sha256(bogus.encode()).digest())
+
+    return ''.join(srandom.choice(allowed_chars) for i in range(length))
+
+
+class SessionAssets:
+    """ Provider for assets of a specific session. Inherited by Session.
+    
+    The responsibility of this class is to keep track of what JSModules
+    are being used, to provide the associated bundles and assets, and to
+    dynamically define assets when needed. Further this class takes
+    care of per-session data.
+    """
+    
+    def __init__(self, store=None):  # Allow custom store for testing
+        self._store = store if (store is not None) else assetstore
+        assert isinstance(self._store, AssetStore)
+        
+        self._id = get_random_string()
+        self._app_name = ''
+        
+        # Keep track of all assets for this session. Assets that are provided
+        # by the asset store have a value of None.
+        self._used_classes = set()  # Model classes registered as used
+        self._used_modules = set()  # module names that define used classes, plus deps
+        self._loaded_modules = set()  # module names that were present in bundles
+        # Data for this session (in addition to the data provided by the store)
+        # todo: get rid of session assets alltogether, or is there a use-case?
+        self._assets = {}
+        self._data = {}
+        # Whether the page has been served already
+        self._served = 0
+        self._is_interactive = None
+    
+    @property
+    def id(self):
+        """ The unique identifier of this session.
+        """
+        return self._id
+    
+    def get_data_names(self):
+        """ Get a list of names of the data provided by this session, in
+        the order that they were added.
+        """
+        return list(self._data.keys())  # Note: order matters
+    
+    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
+        by that name is unknown.
+        """
+        data = self._data.get(name, None)
+        if data is None:
+            data = self._store.get_data(name)
+        return data
+    
+    # todo: the way that we do assets now makes me wonder whether there are better ways
+    # to deal with data handling ...
+    
+    def add_data(self, name, data):  # todo: add option to clear data after its loaded?
+        """ 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 ``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. Can also be a uri to the blob
+                (string starting with "file://", "http://" or "https://").
+        """
+        if not isinstance(name, str):
+            raise TypeError('Session.add_data() name must be a str.')
+        if name in self._data:
+            raise ValueError('Session.add_data() got existing name %r.' % name)
+        if isinstance(data, str):
+            if data.startswith('file://'):
+                data = open(data.split('//', 1)[1], 'rb').read()
+            elif data.startswith(('http://', 'https://')):
+                data = urlopen(data, timeout=5.0).read()
+        if not isinstance(data, bytes):
+            raise TypeError('Session.add_data() data must be a 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.
+        """
+        self._data.pop(name, None)
+    
+    def register_model_class(self, cls):
+        """ Mark the given Model class as used; ensure that the client
+        knows about it.
+        """
+        if not (isinstance(cls, type) and issubclass(cls, Model)):
+            raise ValueError('Not a Model class')
+            
+        # Early exit if we know the class already
+        if cls in self._used_classes:
+            return
+        
+        # Make sure the base classes are registered first
+        for cls2 in cls.mro()[1:]:
+            if not issubclass(cls2, Model):  # True if cls2 is *the* Model class
+                break
+            if cls2 not in self._used_classes:
+                self.register_model_class(cls2)
+        
+        # Ensure interactive flag - e.g. for in the notebook
+        if self._is_interactive is None:
+            self._is_interactive = self._app_name == '__default__'
+        
+        # Make sure that no two models have the same name, or we get problems
+        # that are difficult to debug. Unless classes are defined in the notebook.
+        same_name = [c for c in self._used_classes if c.__name__ == cls.__name__]
+        if same_name:
+            same_name.append(cls)
+            is_dynamic_cls = all([c.__module__ == '__main__' for c in same_name])
+            if not (self._is_interactive and is_dynamic_cls):
+                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._used_classes.add(cls)
+        self._store.update_modules(cls)  # Update module definition
+        self._register_module(cls.__jsmodule__)
+    
+    def _register_module(self, mod_name):
+        """ Mark a module (and its dependencies) as used. If the page is
+        already served, will inject the module dynamically.
+        """
+        
+        if not self._served:
+            # Not served yet, register asset as used so we can serve it later
+            if mod_name not in self._used_modules:
+                self._used_modules.add(mod_name)
+                mod = self._store.modules[mod_name]
+                for dep in mod.deps:
+                    self._register_module(dep)
+        
+        else:
+            # Already served, we might need to load dynamically. We simply
+            # check whether a module is new or has changed since its source
+            # was last obtained. E.g. it could be that its a new class for 
+            # this session, but that it was loaded as part of the bundle.
+            mod = self._store.modules[mod_name]
+            modules = [m for m in self._store.modules.values()
+                       if m.name not in self._loaded_modules or
+                       m.changed_time >= self._served]
+            modules = solve_dependencies(modules)  # sort based on deps
+            if modules:
+                # Bundles - the dash makes this bundle have an empty "module name"
+                js_asset = Bundle('-extra.js')
+                css_asset = Bundle('-extra.css')
+                for mod in modules:
+                    js_asset.add_module(mod)
+                    css_asset.add_module(mod)
+                # Load assets of modules that were not yet used
+                for mod in modules:
+                    if mod.name not in self._used_modules:
+                        for asset in self._store.get_associated_assets(mod.name):
+                            self._inject_asset_dynamically(asset)
+                # Load bundles
+                self._inject_asset_dynamically(css_asset)
+                self._inject_asset_dynamically(js_asset)
+                # Mark the modules as used and loaded
+                for mod in modules:
+                    self._used_modules.add(mod.name)
+    
+    def _inject_asset_dynamically(self, asset):
+        """ Load an asset in a running session.
+        This method assumes that this is a Session class.
+        """
+        logger.debug('Dynamically loading asset %r' % asset.name)
+        
+        in_notebook = (self._is_interactive and
+                       getattr(self, 'init_notebook_done', False))
+        
+        if in_notebook:
+            # Load using IPython constructs
+            from IPython.display import display, HTML
+            if asset.name.lower().endswith('.js'):
+                display(HTML("<script>%s</script>" % asset.to_string()))
+            else:
+                display(HTML("<style>%s</style>" % asset.to_string()))
+        else:
+            # Load using Flexx construct (using Session._send_command())
+            suffix = asset.name.split('.')[-1].upper()
+            self._send_command('DEFINE-%s %s' % (suffix, asset.to_string()))
+    
+    def get_assets_in_order(self, css_reset=False, load_all=None, bundle_level=None):
+        """ Get two lists containing the JS assets and CSS assets,
+        respectively. The assets contain bundles corresponding to all modules
+        being used (and their dependencies). The order of bundles is based on
+        the dependency resolution. The order of other assets is based on the
+        order in which assets were instantiated. Special assets are added, such
+        as the CSS reset and the JS module loader.
+        
+        After this function gets called, it is assumed that the assets have
+        been served and that future asset loads should be done dynamically.
+        """
+        
+        # Make store aware of everything that we know now
+        self._store.update_modules()
+        
+        if load_all is None:
+            load_all = config.bundle_all
+        if load_all:
+            modules_to_load = self._store.modules.keys()  # e.g. notebook
+        else:
+            modules_to_load = self._used_modules
+        
+        # Get bundle names that contain all the used modules. We use
+        # bundledversions, which means that we load more modules than
+        # we use. In this step we can make a lot of choices with regard
+        # to how much modules we want to pack in a bundle. We could use
+        # a different depth per branch, we could create session-specific
+        # bundles, we could allow users to define a bundle, etc. For
+        # now, we just truncate at a certain level.
+        # todo: this could be configurable, e.g. 99 for dev, 1 for prod
+        level = max(1, bundle_level or 2)
+        bundle_names = set()
+        for mod_name in modules_to_load:
+            bundle_names.add('.'.join(mod_name.split('.')[:level]))
+        
+        # Get bundles
+        js_assets = [self._store.get_asset(b + '.js') for b in bundle_names]
+        css_assets = [self._store.get_asset(b + '.css') for b in bundle_names]
+        
+        # Get loaded modules
+        for asset in js_assets:
+            self._loaded_modules.update([m.name for m in asset.modules])
+        
+        # Sort bundles by name and dependency resolution
+        f = lambda m: (m.name.startswith('__main__'), m.name)
+        js_assets = solve_dependencies(sorted(js_assets, key=f))
+        css_assets = solve_dependencies(sorted(css_assets, key=f))
+        
+        # Filter out empty css bundles
+        css_assets = [asset for asset in css_assets
+                      if any([m.get_css().strip() for m in asset.modules])]
+        
+        # Collect non-module assets
+        # Assets only get included if they are in a module that is *used*.
+        asset_deps_before = set()
+        # asset_deps_after = set()
+        for mod_name in self._used_modules:
+            asset_deps_before.update(self._store.get_associated_assets(mod_name))
+        
+        # Push assets in the lists (sorted by the creation time)
+        f = lambda a: a.i
+        for asset in reversed(sorted(asset_deps_before, key=f)):
+            if asset.name.lower().endswith('.js'):
+                js_assets.insert(0, asset)
+            else:
+                css_assets.insert(0, asset)
+       
+        # Mark all assets as used. For now, we only use assets that are available
+        # in the asset store.
+        for asset in js_assets + css_assets:
+            self._assets[asset.name] = None
+        
+        
+        # Prepend reset.css
+        if css_reset:
+            css_assets.insert(0, self._store.get_asset('reset.css'))
+        
+        # Prepend flexx-info, module loader, and pyscript std
+        js_assets.insert(0, self._store.get_asset('pyscript-std.js'))
+        js_assets.insert(0, self._store.get_asset('flexx-loader.js'))
+        t = 'var flexx = {app_name: "%s", session_id: "%s"};'
+        js_assets.insert(0, Asset('flexx-info.js', t % (self._app_name, self.id)))
+        
+        # Mark this session as served; all future asset loads are dynamic
+        self._served = time.time()
+        
+        # todo: fix incorrect order; loader should be able to handle it for JS
+        #import random
+        #random.shuffle(js_assets)
+        
+        return js_assets, css_assets
+    
+    def get_page(self, link=3):
+        """ Get the string for the HTML page to render this session's app.
+        """
+        js_assets, css_assets = self.get_assets_in_order(True)
+        for asset in js_assets + css_assets:
+            if asset.remote and asset.source.startswith('file://'):
+                raise RuntimeError('Can only use remote assets with "file://" '
+                                   'when exporting.')
+        return self._get_page(js_assets, css_assets, link, False)
+    
+    def get_page_for_export(self, commands, link=0):
+        """ Get the string for an exported HTML page (to run without a server).
+        """
+        # Create lines to init app
+        lines = []
+        lines.append('flexx.is_exported = true;\n')
+        lines.append('flexx.runExportedApp = function () {')
+        lines.extend(['    flexx.command(%s);' % reprs(c) for c in commands])
+        lines.append('};\n')
+        # Create a session asset for it, "-export.js" is always embedded
+        export_asset = Asset('flexx-export.js', '\n'.join(lines))
+        # Compose
+        bundle_level = 2 if (link >= 2) else 9
+        js_assets, css_assets = self.get_assets_in_order(css_reset=True,
+                                                         bundle_level=bundle_level)
+        js_assets.append(export_asset)
+        return self._get_page(js_assets, css_assets, link, True)
+    
+    def _get_page(self, js_assets, css_assets, link, export):
+        """ Compose index page.
+        """
+        pre_path = '_assets' if export else '/flexx/assets'
+        
+        codes = []
+        for assets in [css_assets, js_assets]:
+            for asset in assets:
+                if not link:
+                    html = asset.to_html('{}', link)
+                else:
+                    if asset.name.endswith(('-info.js', '-export.js')):
+                        html = asset.to_html('', 0)
+                    elif self._store.get_asset(asset.name) is not asset:
+                        html = asset.to_html(pre_path + '/%s/{}' % self.id, link)
+                    else:
+                        html = asset.to_html(pre_path + '/shared/{}', link)
+                codes.append(html)
+            codes.append('')  # whitespace between css and js assets
+        
+        src = INDEX
+        if not link:
+            asset_names = [a.name for a in css_assets + js_assets]
+            toc = '<!-- Contents:\n\n- ' + '\n- '.join(asset_names) + '\n\n-->'
+            codes.insert(0, toc)
+            src = src.replace('ASSET-HOOK', '\n\n\n'.join(codes))
+        else:
+            src = src.replace('ASSET-HOOK', '\n'.join(codes))
+        
+        return src
+    
+    def _export(self, dirname, clear=False):
+        """ Export all assets and data specific to this session.
+        Private method, used by app.export().
+        """
+        # Note that self.id will have been set to the app name.
+        assets = []
+        data = [(name, self.get_data(name)) for name in self.get_data_names()]
+        export_assets_and_data(assets, data, dirname, self.id, clear)
+        logger.info('Exported assets and data for %r to %r.' % (self.id, dirname))
+
+
+class Session(SessionAssets):
+    """
+    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.
+    """
+    
+    STATUS = new_type('Enum', (), {'PENDING': 1, 'CONNECTED': 2, 'CLOSED': 0})
+    
+    def __init__(self, app_name):
+        super().__init__()
+        
+        self._app_name = app_name  # name of the app, available before the app itself
+        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
+        
+        # A counter to generate model id's, used by the Model class
+        self._modelcounter = 0
+        
+        # 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 = []
+        
+        # Objects that are guarded from deletion: id: (ping_count, instance)
+        self._instances_guarded = {}
+        
+        self._creation_time = time.time()
+    
+    def __repr__(self):
+        s = self.status
+        return '<Session for %r (%i) at 0x%x>' % (self.app_name, s, id(self))
+    
+    @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. Can be None if Flexx
+        is used in interactive mode (using the ``__default__`` 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
+    
+    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
+        the session_id so it can be connected to the correct Session
+        via this method
+        """
+        if self._ws is not None:
+            raise RuntimeError('Session is already connected.')
+        # Set websocket object - this is what changes the status to CONNECTED
+        self._ws = ws  
+        # todo: make icon and title work again. Also in exported docs.
+        # Set some app specifics
+        # self._ws.command('ICON %s.ico' % self.id)
+        # self._ws.command('TITLE %s' % self._config.title)
+        # Send pending commands
+        for command in self._pending_commands:
+            self._ws.command(command)
+        self._ws.command('INIT-DONE')
+   
+    def _set_app(self, model):
+        if self._model is not None:
+            raise RuntimeError('Session already has an associated Model.')
+        self._model = model
+        # todo: connect to title change and icon change events
+    
+    def _set_runtime(self, runtime):
+        if self._runtime is not None:
+            raise RuntimeError('Session already has a runtime.')
+        self._runtime = runtime
+    
+    def close(self):
+        """ Close the session: close websocket, close runtime, dispose app.
+        """
+        # Stop guarding objects to break down any circular refs
+        for id in list(self._instances_guarded.keys()):
+            self._instances_guarded.pop(id)
+        self._closing = True  # suppress warnings for session being closed.
+        try:
+            
+            # Close the websocket
+            if self._ws:
+                self._ws.close_this()
+            # Close the runtime
+            if self._runtime:
+                self._runtime.close()
+            # Dispose the model and break the circular reference
+            if self._model:
+                self._model.dispose()
+                self._model = None
+        finally:
+            self._closing = False
+    
+    @property
+    def status(self):
+        """ The status of this session. The lifecycle for each session is:
+        
+        * status 1: pending
+        * statys 2: connected
+        * status 0: closed
+        """
+        if self._ws is None:
+            return self.STATUS.PENDING  # not connected yet
+        elif self._ws.close_code is None:
+            return self.STATUS.CONNECTED  # alive and kicking
+        else:
+            return self.STATUS.CLOSED  # connection closed
+    
+    def _send_command(self, command):
+        """ Send the command, add to pending queue.
+        """
+        if self._closing:
+            pass
+        elif self.status == self.STATUS.CONNECTED:
+            self._ws.command(command)
+        elif self.status == self.STATUS.PENDING:
+            self._pending_commands.append(command)
+        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.
+        """
+        if command.startswith('RET '):
+            print(command[4:])  # Return value
+        elif command.startswith('ERROR '):
+            logger.error('JS - ' + command[6:].strip() +
+                         ' (stack trace in browser console)')
+        elif command.startswith('WARN '):
+            logger.warn('JS - ' + command[5:].strip())
+        elif command.startswith('PRINT '):
+            print(command[5:].strip())
+        elif command.startswith('INFO '):
+            logger.info('JS - ' + command[5:].strip())
+        elif command.startswith('SET_PROP '):
+            # todo: seems weird to deal with here. implement by registring some handler?
+            # Should be better when we implement a more formal protocol
+            _, id, name, txt = command.split(' ', 3)
+            ob = Model._instances.get(id, None)
+            if ob is not None:
+                ob._set_prop_from_js(name, txt)
+        elif command.startswith('SET_EVENT_TYPES '):
+            _, id, txt = command.split(' ', 3)
+            ob = Model._instances.get(id, None)
+            if ob is not None:
+                ob._set_event_types_js(txt)
+        elif command.startswith('EVENT '):
+            _, id, name, txt = command.split(' ', 3)
+            ob = Model._instances.get(id, None)
+            if ob is not None:
+                ob._emit_from_js(name, txt)
+        else:
+            logger.warn('Unknown command received from JS:\n%s' % command)
+    
+    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
+        objects survive jitter due to synchronisation, though any type
+        of object can be given.
+        """
+        obid = id(ob)
+        counter = 0 if self._ws is None else self._ws.ping_counter
+        lifetime = counter + int(iters)
+        if lifetime > self._instances_guarded.get(obid, (0, ))[0]:
+            self._instances_guarded[obid] = lifetime, ob
+    
+    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
+        "timeout counter" has expired.
+        """
+        objects_to_clear = [ob for c, ob in
+                           self._instances_guarded.values() if c <= count]
+        for ob in objects_to_clear:
+            self._instances_guarded.pop(id(ob))
+    
+    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.
+        """
+        if self._ws is None:
+            raise RuntimeError('App not connected')
+        self._send_command('EVAL ' + code)
diff --git a/flexx/app/tornadoserver.py b/flexx/app/_tornadoserver.py
similarity index 88%
rename from flexx/app/tornadoserver.py
rename to flexx/app/_tornadoserver.py
index 6b584de..419ce08 100644
--- a/flexx/app/tornadoserver.py
+++ b/flexx/app/_tornadoserver.py
@@ -1,6 +1,5 @@
 """
-Serve web page and handle web sockets. Uses Tornado, though this
-can be generalized.
+Serve web page and handle web sockets using Tornado.
 """
 
 import json
@@ -12,19 +11,19 @@ import threading
 from urllib.parse import urlparse
 # from concurrent.futures import ThreadPoolExecutor
 
-import tornado.web
-import tornado.websocket
-import tornado.httpserver
+import tornado
+from tornado import gen, netutil
+from tornado.web import Application, RequestHandler
 from tornado.ioloop import IOLoop
-from tornado import gen
-from tornado import netutil
+from tornado.websocket import WebSocketHandler
+from tornado.httpserver import HTTPServer
 
-from .session import manager
-from .assetstore import assets
+from ._server import AbstractServer
+from ._app import manager
+from ._assetstore import assets
 from . import logger
 from .. import config
 
-
 if tornado.version_info < (4, ):
     raise RuntimeError('Flexx requires Tornado v4.0 or higher.')
 
@@ -42,75 +41,6 @@ def is_main_thread():
     return isinstance(threading.current_thread(), threading._MainThread)
 
 
-class AbstractServer:
-    """ This is an attempt to generalize the server, so that in the
-    future we may have e.g. a Flask or Pyramid server.
-    
-    A server must implement this, and use the manager to instantiate,
-    connect and disconnect sessions. The assets object must be used to
-    server assets to the client.
-    
-    Arguments:
-        host (str): the hostname to serve at
-        port (int): the port to serve at. None or 0 mean to autoselect a port.
-    """
-    
-    def __init__(self, host, port):
-        self._serving = None
-        if host is not False:
-            self._open(host, port)
-            assert self._serving  # Check that subclass set private variable
-        self._running = False
-    
-    def start(self):
-        """ Start the event loop. """
-        if not self._serving:
-            raise RuntimeError('Cannot start a closed or non-serving server!')
-        if self._running:
-            raise RuntimeError('Cannot start a running server.')
-        self._running = True
-        try:
-            self._start()
-        finally:
-            self._running = False
-    
-    def stop(self):
-        """ Stop the event loop. This does not close the connection; the server
-        can be restarted. Thread safe. """
-        self.call_later(0, self._stop)
-    
-    def close(self):
-        """ Close the connection. A closed server cannot be used again. """
-        if self._running:
-            raise RuntimeError('Cannot close a running server; need to stop first.')
-        self._serving = None
-        self._close()
-    
-    def _open(self, host, port):
-        raise NotImplementedError()
-    
-    def _start(self):
-        raise NotImplementedError()
-    
-    def _stop(self):
-        raise NotImplementedError()
-    
-    def _close(self):
-        raise NotImplementedError()
-    
-    # This method must be implemented directly for performance (its used a lot)
-    def call_later(self, delay, callback, *args, **kwargs):
-        """ Call a function in a later event loop iteration. """
-        raise NotImplementedError()
-    
-    @property
-    def serving(self):
-        """ Get a tuple (hostname, port) that is being served.
-        Or None if the server is not serving (anymore).
-        """
-        return self._serving
-
-
 class TornadoServer(AbstractServer):
     """ Flexx Server implemented in Tornado.
     """
@@ -136,11 +66,11 @@ class TornadoServer(AbstractServer):
         # run Flexx in e.g. JLab's application.
         
         # Create tornado application
-        self._app = tornado.web.Application([(r"/flexx/ws/(.*)", WSHandler),
-                                             (r"/flexx/(.*)", MainHandler),
-                                             (r"/(.*)", AppHandler), ])
+        self._app = Application([(r"/flexx/ws/(.*)", WSHandler),
+                                 (r"/flexx/(.*)", MainHandler),
+                                 (r"/(.*)", AppHandler), ])
         # Create tornado server, bound to our own ioloop
-        self._server = tornado.httpserver.HTTPServer(self._app, io_loop=self._loop)
+        self._server = HTTPServer(self._app, io_loop=self._loop)
         
         # Start server (find free port number if port not given)
         if port:
@@ -240,7 +170,7 @@ def port_hash(name):
     return 49152 + (val % 2**14)
 
 
-class FlexxHandler(tornado.web.RequestHandler):
+class FlexxHandler(RequestHandler):
     """ Base class for Flexx' Tornado request handlers.
     """
     
@@ -356,7 +286,7 @@ class AppHandler(FlexxHandler):
             self.write(session.get_page().encode())
 
 
-class MainHandler(tornado.web.RequestHandler):
+class MainHandler(RequestHandler):
     """ Handler for assets, commands, etc. Basically, everything for
     which te path is clear.
     """
@@ -544,7 +474,7 @@ class MessageCounter:
         self._stop = True
 
 
-class WSHandler(tornado.websocket.WebSocketHandler):
+class WSHandler(WebSocketHandler):
     """ Handler for websocket.
     """
     
@@ -703,9 +633,9 @@ class WSHandler(tornado.websocket.WebSocketHandler):
     
     def close(self, *args):
         try:
-            tornado.websocket.WebSocketHandler.close(self, *args)
+            WebSocketHandler.close(self, *args)
         except TypeError:
-            tornado.websocket.WebSocketHandler.close(self)  # older Tornado
+            WebSocketHandler.close(self)  # older Tornado
     
     def close_this(self):
         """ Call this to close the websocket
diff --git a/flexx/app/funcs.py b/flexx/app/funcs.py
deleted file mode 100644
index a824bda..0000000
--- a/flexx/app/funcs.py
+++ /dev/null
@@ -1,575 +0,0 @@
-"""
-Functional API for flexx.app
-"""
-
-import os
-import sys
-import json
-
-from .. import webruntime, config, set_log_level
-
-from . import model, logger
-from .model import Model
-from .session import manager
-from .assetstore import assets
-from .tornadoserver import TornadoServer
-from ..event import _loop
-
-reprs = json.dumps
-
-## Main loop functions
-
-
-# There is always a single current server (except initially there is None)
-_current_server = None
-
-
-def create_server(host=None, port=None, new_loop=False, backend='tornado'):
-    """
-    Create a new server object. This is automatically called; users generally
-    don't need this, unless they want to explicitly specify host/port,
-    create a fresh server in testing scenarios, or run Flexx in a thread.
-    
-    Flexx uses a notion of a single current server object. This function
-    (re)creates that object. If there already was a server object, it is
-    replaced. It is an error to call this function if the current server
-    is still running.
-    
-    Arguments:
-        host (str): The hostname to serve on. By default
-            ``flexx.config.hostname`` is used. If ``False``, do not listen
-            (e.g. when integrating with an existing Tornado application).
-        port (int, str): The port number. If a string is given, it is
-            hashed to an ephemeral port number. By default
-            ``flexx.config.port`` is used.
-        new_loop (bool): Whether to create a fresh Tornado IOLoop instance,
-            which is made current when ``start()`` is called. If ``False``
-            (default) will use the current IOLoop for this thread.
-        backend (str): Stub argument; only Tornado is currently supported.
-    
-    Returns:
-        server: The server object, see ``current_server()``.
-    """
-    global _current_server
-    if backend.lower() != 'tornado':
-        raise RuntimeError('Flexx server can only run on Tornado (for now).')
-    # Handle defaults
-    if host is None:
-        host = config.hostname
-    if port is None:
-        port = config.port
-    # Stop old server
-    if _current_server:
-        _current_server.close()
-    # Start hosting
-    _current_server = TornadoServer(host, port, new_loop)
-    # Schedule pending calls
-    _current_server.call_later(0, _loop.loop.iter)
-    while _pending_call_laters:
-        delay, callback, args, kwargs = _pending_call_laters.pop(0)
-        call_later(delay, callback, *args, **kwargs)
-    return _current_server
-
-
-def current_server():
-    """
-    Get the current server object. Creates a server if there is none.
-    Currently, this is always a TornadoServer object, which has properties:
-    
-    * serving: a tuple ``(hostname, port)`` specifying the location
-      being served (or ``None`` if the server is closed).
-    * app: the ``tornado.web.Application`` instance
-    * loop: the ``tornado.ioloop.IOLoop`` instance
-    * server: the ``tornado.httpserver.HttpServer`` instance
-    """
-    if not _current_server:
-        create_server()
-    return _current_server
-
-
-def start():
-    """
-    Start the server and event loop. This function generally does not
-    return until the application is stopped (although it may in
-    interactive environments (e.g. Pyzo)).
-    """
-    server = current_server()
-    logger.info('Starting Flexx event loop.')
-    server.start()
-
-
-def run():
-    """
-    Start the event loop in desktop app mode; the server will close
-    down when there are no more connections.
-    """
-    server = current_server()
-    server._auto_stop = True
-    return start()
-
-
-def stop():
-    """
-    Stop the event loop. This function is thread safe (it can be used
-    even if ``flexx.start()`` was called from another thread). 
-    The server can be restarted after it has been stopped. Note that
-    calling ``stop()`` too often will cause a subsequent call to `start()``
-    to return almost immediately.
-    """
-    server = current_server()
-    server.stop()
-
-
-def call_later(delay, callback, *args, **kwargs):
-    """
-    Schedule a function call in the current event loop. This function is
-    thread safe.
-    
-    Arguments:
-        delay (float): the delay in seconds. If zero, the callback will
-            be executed in the next event loop iteration.
-        callback (callable): the function to call.
-        args: the positional arguments to call the callback with.
-        kwargs: the keyword arguments to call the callback with.
-    """
-    if not _current_server:
-        _pending_call_laters.append((delay, callback, args, kwargs))
-        return
-    server = current_server()
-    server.call_later(delay, callback, *args, **kwargs)
-
-
-# Work around circular dependency
-model.call_later = call_later
-
-_pending_call_laters = []
-
-# Integrate the "event-loop" of flexx.event
-_loop.loop.integrate(lambda f: call_later(0, f))
-
-
-@manager.connect('connections_changed')
-def _auto_closer(*events):
-    server = current_server()
-    if not getattr(server, '_auto_stop', False):
-        return
-    for name in manager.get_app_names():
-        proxies = manager.get_connections(name)
-        if proxies:
-            return
-    else:
-        logger.info('Stopping Flexx event loop.')
-        server.stop()
-
-
-## App functions
-
-
-def init_interactive(cls=None, runtime=None):
-    """ Initialize Flexx for interactive mode. This creates a default session
-    and launches a runtime to connect to it. 
-    
-    Parameters:
-        cls (None, Model): a subclass of ``app.Model`` (or ``ui.Widget``) to use
-            as the *default active model*. Only has effect the first time that
-            this function is called.
-        runtime (str): the runtime to launch the application in. Default 'xul'.
-    """
-    
-    # Determine default model class (which is a Widget if ui is imported)
-    if cls is None and 'flexx.ui' in sys.modules:
-        from .. import ui
-        cls = ui.Widget
-    
-    # Create the default session
-    session = manager.get_default_session()
-    if session is None:
-        session = manager.create_default_session(cls)
-    else:
-        return  # default session already running
-
-    # Launch web runtime, the server will wait for the connection
-    server = current_server()
-    host, port = server.serving
-    url = '%s:%i/%s/?session_id=%s' % (host, port, session.app_name, session.id)
-    session._runtime = launch('http://' + url, runtime=runtime)
-    
-
-class App:
-    """ Specification of a Flexx class.
-    
-    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.')
-        self._cls = cls
-        self.args = args
-        self.kwargs = kwargs
-        self._path = cls.__name__  # can be overloaded by serve()
-        self._is_served = False
-    
-    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
-        been called yet or if Flexx' server is not yet running.
-        """
-        if not self._is_served:
-            raise RuntimeError('Cannot determine app url if app is not yet "served".')
-        elif not (_current_server and _current_server.serving):
-            raise RuntimeError('Cannot determine app url if the server is not '
-                               'yet running.')
-        else:
-            host, port = _current_server.serving
-            return 'http://%s:%i/%s/' % (host, port, self._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.
-        """
-        # Note: this talks to the manager; it has nothing to do with the server
-        if self._is_served:
-            raise RuntimeError('This app (%s) is already served.' % self.name)
-        if name is not None:
-            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 'xul'.
-            runtime_kwargs: kwargs to pass to the ``webruntime.launch`` function.
-        
-        Returns:
-            app (Model): an instance of the given class.
-        """
-        # Create session
-        if not self._is_served:
-            self.serve()
-        session = manager.create_session(self.name)
-        
-        # Launch web runtime, the server will wait for the connection
-        current_server()  # creates server if it did not yet exist
-        if runtime == 'nodejs':
-            js_assets, _ = session.get_assets_in_order()
-            all_js = '\n\n'.join([asset.to_string() for asset in js_assets])
-            session._runtime = launch(self.url, runtime=runtime, code=all_js)
-        else:
-            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).
-                * 3: (default) normal assets are linked, remote assets remain remote.
-            write_shared (bool): if True (default) will also write shared assets
-                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 = session.get_page_for_export(exporter._commands, link)
-        if filename is None:
-            return html
-        elif filename.lower().endswith('.hta'):
-            hta_tag = '<meta http-equiv="x-ua-compatible" content="ie=edge" />'
-            html = html.replace('<head>', '<head>\n    ' + hta_tag, 1)
-        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))
-        if link:
-            if write_shared:
-                assets.export(os.path.dirname(filename))
-            session._export(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))
-
-
-class NoteBookHelper:
-    """ Object that captures commands send to the websocket during the
-    execution of a cell, and then applies these commands using a script
-    node. This way, Flexx widgets keep working in the exported notebook.
-    """
-    
-    close_code = None
-    
-    def __init__(self, session):
-        self._session = session
-        self._real_ws = None
-        self._commands = []
-        self.enable()
-    
-    def enable(self):
-        from IPython import get_ipython
-        ip = get_ipython()
-        ip.events.register('pre_execute', self.capture)
-        ip.events.register('post_execute', self.release)
-    
-    def capture(self):
-        if self._real_ws is not None:
-            logger.warn('Notebookhelper already is in capture mode.')
-        else:
-            assert self._session._ws is not None
-            self._real_ws = self._session._ws
-            self._session._ws = self
-    
-    def release(self):
-        if self._session._ws is self:
-            self._session._ws = self._real_ws
-        self._real_ws = None
-        if self._commands:
-            from IPython.display import display, Javascript
-            commands = ['flexx.command(%s);' % reprs(msg) for msg in self._commands]
-            self._commands = []
-            display(Javascript('\n'.join(commands)))
-    
-    def command(self, msg):
-        self._commands.append(msg)
-    
-    @property
-    def ping_counter(self):
-        if self._session._ws is self:
-            return self._real_ws.ping_counter
-        else:
-            return self._session._ws.ping_counter
-
-
-def init_notebook():
-    """ Initialize the Jupyter notebook by injecting the necessary CSS
-    and JS into the browser. Note that any Flexx-based libraries that
-    you plan to use should probably be imported *before* calling this.
-    """
-    
-    # Note: not using IPython Comm objects yet, since they seem rather
-    # undocumented and I could not get them to work when I tried for a bit.
-    # This means though, that flexx in the notebook only works on localhost.
-    
-    from IPython.display import display, clear_output, HTML
-    # from .. import ui  # noqa - make ui assets available
-    
-    # Make default log level warning instead of "info" to avoid spamming
-    # This preserves the log level set by the user
-    config.load_from_string('log_level = warning', 'init_notebook')
-    set_log_level(config.log_level)
-    
-    # Get session or create new
-    session = manager.get_default_session()
-    if session is None:
-        session = manager.create_default_session()
-    
-    # Open server - the notebook helper takes care of the JS resulting
-    # from running a cell, but any interaction goes over the websocket.
-    server = current_server()
-    host, port = server.serving
-    
-    # Trigger loading phosphor assets
-    if 'flexx.ui' in sys.modules:
-        from flexx import ui
-        session.register_model_class(ui.Widget)
-    
-    # Get assets, load all known modules to prevent dynamic loading as much as possible
-    js_assets, css_assets = session.get_assets_in_order(css_reset=False, load_all=True)
-    
-    # Pop the first JS asset that sets flexx.app_name and flexx.session_id
-    # We set these in a way that it does not end up in exported notebook.
-    js_assets.pop(0)
-    url = 'ws://%s:%i/flexx/ws/%s' % (host, port, session.app_name)
-    flexx_pre_init = """<script>window.flexx = window.flexx || {};
-                                window.flexx.app_name = "%s";
-                                window.flexx.session_id = "%s";
-                                window.flexx.ws_url = "%s";
-                                window.flexx.is_live_notebook = true;
-                        </script>""" % (session.app_name, session.id, url)
-    
-    # Check if already loaded, if so, re-connect
-    if not getattr(session, 'init_notebook_done', False):
-        session.init_notebook_done = True  # also used in assetstore
-    else:
-        display(HTML(flexx_pre_init))
-        clear_output()
-        display(HTML("""<script>
-                        flexx.is_exported = !flexx.is_live_notebook;
-                        flexx.init();
-                        </script>
-                        <i>Flexx already loaded. Reconnected.</i>
-                        """))
-        return  # Don't inject Flexx twice
-        # Note that exporting will not work anymore since out assets
-        # are no longer in the outputs
-    
-    # Install helper to make things work in exported notebooks
-    NoteBookHelper(session)
-    
-    # Compose HTML to inject
-    t = "<i>Injecting Flexx JS and CSS</i>"
-    t += '\n\n'.join([asset.to_html('{}', 0) for asset in css_assets + js_assets])
-    t += """<script>
-            flexx.is_notebook = true;
-            flexx.is_exported = !flexx.is_live_notebook;
-            /* If Phosphor is already loaded, disable our Phosphor CSS. */
-            if (window.jupyter && window.jupyter.lab) {
-                document.getElementById('phosphor-all.css').disabled = true;
-            }
-            flexx.init();
-            </script>"""
-    
-    display(HTML(flexx_pre_init))  # Create initial Flexx info dict
-    clear_output()  # Make sure the info dict is gone in exported notebooks
-    display(HTML(t))
-    
-    # Note: the Widget._repr_html_() method is responsible for making
-    # the widget show up in the notebook output area.
-
-
-# todo: deprecate these
-
-def serve(cls, name=None, properties=None):
-    """ Backwards compat.
-    """
-    # Note: this talks to the manager; it has nothing to do with the server
-    assert (isinstance(cls, type) and issubclass(cls, Model))
-    a = App(cls, **(properties or {}))
-    a.serve(name)
-    return cls
-
-
-def launch(cls, runtime=None, properties=None, **runtime_kwargs):
-    """ Backwards compat.
-    """
-    if isinstance(cls, str):
-        return webruntime.launch(cls, runtime, **runtime_kwargs)
-    assert (isinstance(cls, type) and issubclass(cls, Model))
-    a = App(cls, **(properties or {}))
-    return a.launch(runtime, **runtime_kwargs)
-
-
-def export(cls, filename=None, properties=None, single=None, link=None,
-           write_shared=True):
-    """ Backward compat.
-    """
-    if not (isinstance(cls, type) and issubclass(cls, Model)):
-        raise ValueError('runtime must be a string or Model subclass.')
-    
-    # Backward comp - deprecate "single" argument at some point
-    if link is None:
-        if single is not None:
-            logger.warn('Export single arg is deprecated, use link instead.')
-            if not single:
-                link = 3
-    link = int(link or 0)
-    
-    a = App(cls, **(properties or {}))
-    return a.export(filename, link=link, write_shared=write_shared)
-
-
-class ExporterWebSocketDummy:
-    """ Object that can be used by an app inplace of the websocket to
-    export apps to standalone HTML. The object tracks the commands send
-    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', 
-        #                                                       'Exported flexx app'))
-    
-    def command(self, cmd):
-        self._commands.append(cmd)


Test patch:
diff --git a/flexx/app/tests/test_asset.py b/flexx/app/tests/test_asset.py
index 1bfc299..8ab75ae 100644
--- a/flexx/app/tests/test_asset.py
+++ b/flexx/app/tests/test_asset.py
@@ -11,7 +11,7 @@ import shutil
 
 from flexx.util.testing import run_tests_if_main, raises
 
-from flexx.app.asset import solve_dependencies
+from flexx.app._asset import solve_dependencies
 from flexx.util.logging import capture_log
 from flexx import ui, app
 
@@ -212,9 +212,9 @@ def test_bundle():
     assert bundle.modules == (m3, m1, m2)
     
     # Deps are agregated
-    assert 'flexx.app.clientcore.js' in bundle.deps
+    assert 'flexx.app._clientcore.js' in bundle.deps
     assert 'flexx.app.js' in bundle.deps
-    assert 'flexx.app.model.js' in bundle.deps
+    assert 'flexx.app._model.js' in bundle.deps
     assert not any('flexx.ui' in dep for dep in bundle.deps)
     
     # Strings are combined
diff --git a/flexx/app/tests/test_assetstore.py b/flexx/app/tests/test_assetstore.py
index 298e2b9..8a35c38 100644
--- a/flexx/app/tests/test_assetstore.py
+++ b/flexx/app/tests/test_assetstore.py
@@ -11,7 +11,8 @@ import shutil
 
 from flexx.util.testing import run_tests_if_main, raises
 
-from flexx.app.assetstore import assets, AssetStore, SessionAssets
+from flexx.app._assetstore import assets, AssetStore
+from flexx.app._session import SessionAssets
 
 from flexx import ui, app
 
@@ -29,14 +30,14 @@ def test_asset_store_collect():
     s.update_modules()
     assert len(s.modules) > 10
     assert 'flexx.ui._widget' in s.modules
-    assert 'flexx.app.model' in s.modules
+    assert 'flexx.app._model' in s.modules
     
     assert '.Widget =' in s.get_asset('flexx.ui._widget.js').to_string()
     assert '.Widget =' in s.get_asset('flexx.ui.js').to_string()
     assert '.Widget =' in s.get_asset('flexx.js').to_string()
     assert '.Widget =' not in s.get_asset('flexx.app.js').to_string()
     
-    assert '.Model =' in s.get_asset('flexx.app.model.js').to_string()
+    assert '.Model =' in s.get_asset('flexx.app._model.js').to_string()
     assert '.Model =' in s.get_asset('flexx.app.js').to_string()
     assert '.Model =' in s.get_asset('flexx.js').to_string()
     assert '.Model =' not in s.get_asset('flexx.ui.js').to_string()
@@ -255,12 +256,12 @@ def test_session_registering_model_classes():
     assert ui.Button.__jsmodule__ in s._used_modules
     assert ui.BaseButton.__jsmodule__ in s._used_modules
     assert ui.Widget.__jsmodule__ in s._used_modules
-    assert 'flexx.app.model' in s._used_modules
+    assert 'flexx.app._model' in s._used_modules
     
     # Get assets, level 9
     js_assets, css_assets = s.get_assets_in_order(bundle_level=9)
     names = [a.name for a in js_assets]
-    assert 'flexx.app.model.js' in names
+    assert 'flexx.app._model.js' in names
     assert 'flexx.ui.widgets._button.js' in names
     
     # Get assets, level 2
diff --git a/flexx/app/tests/test_model.py b/flexx/app/tests/test_model.py
index 9fdf246..5158222 100644
--- a/flexx/app/tests/test_model.py
+++ b/flexx/app/tests/test_model.py
@@ -8,7 +8,7 @@ import gc
 import logging
 import tornado
 
-from flexx.app.model import Model, _get_active_models
+from flexx.app._model import Model, _get_active_models
 from flexx import event, app
 
 class Foo1(Model):
diff --git a/flexx/app/tests/test_module.py b/flexx/app/tests/test_module.py
index 33655dd..d0eb911 100644
--- a/flexx/app/tests/test_module.py
+++ b/flexx/app/tests/test_module.py
@@ -13,7 +13,7 @@ from flexx.util.testing import run_tests_if_main, raises
 from flexx.util.logging import capture_log
 
 from flexx import ui, app
-from flexx.app.modules import JSModule
+from flexx.app._modules import JSModule
 
 tempdirname = os.path.join(tempfile.gettempdir(), 'flexx_module_test')
 
@@ -185,7 +185,7 @@ def test_modules():
     assert 'flxtest.lib2' in store
     assert 'flxtest.lib3' in store
     assert 'flxtest.__init__' in store  # different from how Python works!
-    assert 'flexx.app.model' in store  # + what it depends on
+    assert 'flexx.app._model' in store  # + what it depends on
     
     # CSS
     assert 'foo-css-rule' in store['flxtest.foo'].get_css()
@@ -261,7 +261,7 @@ def test_misc():
     #
     m.add_variable('Foo')
     assert len(m.deps) == 3
-    assert 'flexx.app.model' in m.deps
+    assert 'flexx.app._model' in m.deps
 
 
 def test_add_variable():
@@ -348,7 +348,7 @@ def test_subclasses():
     #
     m.add_variable('Spam')
     assert 'flxtest.foo' in store
-    assert 'flexx.app.model' in store
+    assert 'flexx.app._model' in store
     
     # Using Foo in modules that imports it
     store = {}
@@ -357,7 +357,7 @@ def test_subclasses():
     #
     m.add_variable('Foo')
     assert 'flxtest.foo' in store
-    assert 'flexx.app.model' in store
+    assert 'flexx.app._model' in store
 
 
 def test_fails():
diff --git a/flexx/app/tests/test_module_loading.py b/flexx/app/tests/test_module_loading.py
new file mode 100644
index 0000000..3641623
--- /dev/null
+++ b/flexx/app/tests/test_module_loading.py
@@ -0,0 +1,43 @@
+import time
+
+from flexx.util.testing import run_tests_if_main, raises
+from flexx.util.logging import capture_log
+
+from flexx import app
+from flexx.app._assetstore import AssetStore
+from flexx.app._session import SessionAssets
+from flexx.app._model import new_type
+
+
+class FakeModule:
+    """ This looks enough like a module for a Bundle to process it.
+    """
+    def __init__(self, name, deps):
+        self.name = name
+        self.deps = tuple(deps)
+        self.changed_time = time.time()
+    
+    def get_js(self):
+        return '<%s>' % self.name
+    
+    def get_css(self):
+        return '<%s>' % self.name
+
+
+class FakeModel:
+    __jsmodule__ = 'module0'  # needs to be set in each subclass
+
+
+def test_simple():
+    
+    store = AssetStore()
+    s = SessionAssets(store)
+    
+    m1 = FakeModule('module1', ())
+    
+    M1 = new_type('M1', (FakeModel, ), {'__jsmodule__': m1.name})
+
+
+test_simple()
+#run_tests_if_main()
+    
\ No newline at end of file
diff --git a/flexx/app/tests/test_serializer.py b/flexx/app/tests/test_serializer.py
index bba6f75..7974e60 100644
--- a/flexx/app/tests/test_serializer.py
+++ b/flexx/app/tests/test_serializer.py
@@ -3,7 +3,7 @@ from flexx.util.testing import run_tests_if_main, raises
 
 from flexx.pyscript import py2js, evaljs
 
-from flexx.app.clientcore import Serializer, serializer
+from flexx.app._clientcore import Serializer, serializer
 
 
 class Foo: