Browse Source

Merge pull request #234 from zauberzeug/client_connections

Rework of client connect and disconnect behaviour
Falko Schindler 2 years ago
parent
commit
5e9d40954f

+ 2 - 2
examples/3d_scene/main.py

@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
-from nicegui import ui
+from nicegui import app, ui
 
-ui.add_static_files('/stl', 'static')
+app.add_static_files('/stl', 'static')
 
 with ui.scene(width=1024, height=800) as scene:
     scene.spot_light(distance=100, intensity=0.1).move(-10, 0, 10)

+ 1 - 1
examples/infinite_scroll/main.py

@@ -9,7 +9,7 @@ async def page(client: Client):
     async def check():
         if await ui.run_javascript('window.pageYOffset >= document.body.offsetHeight - 2 * window.innerHeight'):
             ui.image(f'https://picsum.photos/640/360?{time.time()}')
-    await client.handshake()
+    await client.connected()
     ui.timer(0.1, check)
 
 

+ 1 - 1
examples/map/main.py

@@ -15,7 +15,7 @@ locations = {
 async def main_page(client: Client):
     map = leaflet().classes('w-full h-96')
     selection = ui.select(locations, on_change=lambda e: map.set_location(e.value)).classes('w-40')
-    await client.handshake()  # wait for websocket connection
+    await client.connected()  # wait for websocket connection
     selection.set_value(next(iter(locations)))  # trigger map.set_location with first location in selection
 
 

+ 2 - 2
examples/slideshow/main.py

@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 from pathlib import Path
 
-from nicegui import ui
+from nicegui import app, ui
 from nicegui.events import KeyEventArguments
 
 folder = Path(__file__).resolve().parent / 'slides'  # image source: https://pixabay.com/
@@ -20,7 +20,7 @@ def handle_key(event: KeyEventArguments) -> None:
         slide.set_source(f'slides/{files[index]}')
 
 
-ui.add_static_files('/slides', folder)  # serve all files in this folder
+app.add_static_files('/slides', folder)  # serve all files in this folder
 slide = ui.image(f'slides/{files[index]}')  # show the first image
 ui.keyboard(on_key=handle_key)  # handle keyboard events
 

+ 2 - 2
main.py

@@ -21,8 +21,8 @@ from website.style import example_link, features, heading, link_target, section_
 
 prometheus.start_monitor(app)
 
-ui.add_static_files('/favicon', str(Path(__file__).parent / 'website' / 'favicon'))
-ui.add_static_files('/fonts', str(Path(__file__).parent / 'website' / 'fonts'))
+app.add_static_files('/favicon', str(Path(__file__).parent / 'website' / 'favicon'))
+app.add_static_files('/fonts', str(Path(__file__).parent / 'website' / 'fonts'))
 
 # NOTE in our global fly.io deployment we need to make sure that the websocket connects back to the same instance
 fly_instance_id = os.environ.get('FLY_ALLOC_ID', '').split('-')[0]

+ 61 - 0
nicegui/app.py

@@ -0,0 +1,61 @@
+from typing import Awaitable, Callable, Union
+
+from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
+
+from . import globals
+
+
+class App(FastAPI):
+
+    def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
+        """Called every time a new client connects to NiceGUI.
+
+        The callback has an optional parameter of `nicegui.Client`.
+        """
+        globals.connect_handlers.append(handler)
+
+    def on_disconnect(self, handler: Union[Callable, Awaitable]) -> None:
+        """Called every time a new client disconnects from NiceGUI.
+
+        The callback has an optional parameter of `nicegui.Client`.
+        """
+        globals.disconnect_handlers.append(handler)
+
+    def on_startup(self, handler: Union[Callable, Awaitable]) -> None:
+        """Called when NiceGUI is started or restarted.
+
+        Needs to be called before `ui.run()`.
+        """
+        if globals.state == globals.State.STARTED:
+            raise RuntimeError('Unable to register another startup handler. NiceGUI has already been started.')
+        globals.startup_handlers.append(handler)
+
+    def on_shutdown(self, handler: Union[Callable, Awaitable]) -> None:
+        """Called when NiceGUI is shut down or restarted.
+
+        When NiceGUI is shut down or restarted, all tasks still in execution will be automatically canceled.
+        """
+        globals.shutdown_handlers.append(handler)
+
+    async def shutdown(self) -> None:
+        """Programmatically shut down NiceGUI.
+
+        Only possible when auto-reload is disabled.
+        """
+        if globals.reload:
+            raise Exception('calling shutdown() is not supported when auto-reload is enabled')
+        globals.server.should_exit = True
+
+    def add_static_files(self, path: str, directory: str) -> None:
+        """Add static files.
+
+        `add_static_files()` makes a local directory available at the specified endpoint, e.g. `'/static'`.
+        This is useful for providing local data like images to the frontend.
+        Otherwise the browser would not be able to access the files.
+        Do only put non-security-critical files in there, as they are accessible to everyone.
+
+        :param path: string that starts with a slash "/"
+        :param directory: folder with static files to serve under the given path
+        """
+        globals.app.mount(path, StaticFiles(directory=directory))

+ 19 - 5
nicegui/client.py

@@ -29,7 +29,8 @@ class Client:
 
         self.elements: Dict[int, Element] = {}
         self.next_element_id: int = 0
-        self.is_waiting_for_handshake: bool = False
+        self.is_waiting_for_connection: bool = False
+        self.is_waiting_for_disconnect: bool = False
         self.environ: Optional[Dict[str, Any]] = None
         self.shared = shared
 
@@ -83,17 +84,30 @@ class Client:
             'socket_io_js_extra_headers': globals.socket_io_js_extra_headers,
         }, status_code, {'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'})
 
-    async def handshake(self, timeout: float = 3.0, check_interval: float = 0.1) -> None:
-        self.is_waiting_for_handshake = True
+    async def connected(self, timeout: float = 3.0, check_interval: float = 0.1) -> None:
+        '''Blocks execution until the client is connected.'''
+        self.is_waiting_for_connection = True
         deadline = time.time() + timeout
         while not self.environ:
             if time.time() > deadline:
-                raise TimeoutError(f'No handshake after {timeout} seconds')
+                raise TimeoutError(f'No connection after {timeout} seconds')
             await asyncio.sleep(check_interval)
-        self.is_waiting_for_handshake = False
+        self.is_waiting_for_connection = False
+
+    async def disconnected(self, check_interval: float = 0.1) -> None:
+        '''Blocks execution until the client disconnects.'''
+        self.is_waiting_for_disconnect = True
+        while self.id in globals.clients:
+            await asyncio.sleep(check_interval)
+        self.is_waiting_for_disconnect = False
 
     async def run_javascript(self, code: str, *,
                              respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Optional[str]:
+        '''Allows execution of javascript on the client.
+
+        The client connection must be established before this method is called.
+        You can do this by `await client.connected()` or register a callback with `client.on_connected(...)`.
+        If respond is True, the javascript code must return a string.'''
         request_id = str(uuid.uuid4())
         command = {
             'code': code,

+ 1 - 2
nicegui/events.py

@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any, BinaryIO, Callable, List, Optional
 from . import background_tasks, globals
 from .async_updater import AsyncUpdater
 from .client import Client
-from .functions.lifecycle import on_startup
 from .helpers import is_coroutine
 
 if TYPE_CHECKING:
@@ -275,6 +274,6 @@ def handle_event(handler: Optional[Callable], arguments: EventArguments) -> None
             if globals.loop and globals.loop.is_running():
                 background_tasks.create(wait_for_result(), name=str(handler))
             else:
-                on_startup(wait_for_result())
+                globals.app.on_startup(wait_for_result())
     except Exception:
         traceback.print_exc()

+ 2 - 1
nicegui/functions/javascript.py

@@ -7,6 +7,7 @@ async def run_javascript(code: str, *,
                          respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Optional[str]:
     client = globals.get_client()
     if not client.has_socket_connection:
-        raise RuntimeError('Cannot run JavaScript before client handshake.')
+        raise RuntimeError(
+            'Cannot run JavaScript before client is connected; try "await client.connected()" or "client.on_connect(...)".')
 
     return await client.run_javascript(code, respond=respond, timeout=timeout, check_interval=check_interval)

+ 0 - 27
nicegui/functions/lifecycle.py

@@ -1,27 +0,0 @@
-from typing import Awaitable, Callable, Union
-
-from .. import globals
-
-
-def on_connect(handler: Union[Callable, Awaitable]) -> None:
-    globals.get_client().connect_handlers.append(handler)
-
-
-def on_disconnect(handler: Union[Callable, Awaitable]) -> None:
-    globals.get_client().disconnect_handlers.append(handler)
-
-
-def on_startup(handler: Union[Callable, Awaitable]) -> None:
-    if globals.state == globals.State.STARTED:
-        raise RuntimeError('Unable to register another startup handler. NiceGUI has already been started.')
-    globals.startup_handlers.append(handler)
-
-
-def on_shutdown(handler: Union[Callable, Awaitable]) -> None:
-    globals.shutdown_handlers.append(handler)
-
-
-async def shutdown() -> None:
-    if globals.reload:
-        raise Exception('ui.shutdown is not supported when auto-reload is enabled')
-    globals.server.should_exit = True

+ 0 - 17
nicegui/functions/static_files.py

@@ -1,17 +0,0 @@
-from fastapi.staticfiles import StaticFiles
-
-from .. import globals
-
-
-def add_static_files(path: str, directory: str) -> None:
-    """Static Files
-
-    `ui.add_static_files` makes a local directory available at the specified endpoint, e.g. `'/static'`.
-    This is useful for providing local data like images to the frontend.
-    Otherwise the browser would not be able to access the files.
-    Do only put non-security-critical files in there, as they are accessible to everyone.
-
-    :param path: string that starts with a slash "/"
-    :param directory: folder with static files to serve under the given path
-    """
-    globals.app.mount(path, StaticFiles(directory=directory))

+ 6 - 7
nicegui/functions/timer.py

@@ -7,7 +7,6 @@ from .. import background_tasks, globals
 from ..async_updater import AsyncUpdater
 from ..binding import BindableProperty
 from ..helpers import is_coroutine
-from .lifecycle import on_startup
 
 
 class Timer:
@@ -35,18 +34,18 @@ class Timer:
         if globals.state == globals.State.STARTED:
             background_tasks.create(coroutine(), name=str(callback))
         else:
-            on_startup(coroutine)
+            globals.app.on_startup(coroutine)
 
     async def _run_once(self) -> None:
         with self.slot:
-            await self._handshake()
+            await self._connected()
             await asyncio.sleep(self.interval)
             await self._invoke_callback()
         self.cleanup()
 
     async def _run_in_loop(self) -> None:
         with self.slot:
-            await self._handshake()
+            await self._connected()
             while True:
                 if self.slot.parent.client.id not in globals.clients:
                     break
@@ -71,12 +70,12 @@ class Timer:
         except Exception:
             traceback.print_exc()
 
-    async def _handshake(self) -> None:
-        '''Wait for the client handshake before the timer callback can can be allowed to manipulate the state.
+    async def _connected(self) -> None:
+        '''Wait for the client connection before the timer callback can can be allowed to manipulate the state.
         See https://github.com/zauberzeug/nicegui/issues/206 for details.
         '''
         if not self.slot.parent.client.shared:
-            await self.slot.parent.client.handshake()
+            await self.slot.parent.client.connected()
 
     def cleanup(self) -> None:
         self.slot = None

+ 5 - 2
nicegui/globals.py

@@ -4,10 +4,11 @@ from contextlib import contextmanager
 from enum import Enum
 from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Union
 
-from fastapi import FastAPI
 from socketio import AsyncServer
 from uvicorn import Server
 
+from .app import App
+
 if TYPE_CHECKING:
     from .client import Client
     from .slot import Slot
@@ -20,7 +21,7 @@ class State(Enum):
     STOPPING = 3
 
 
-app: FastAPI
+app: App
 sio: AsyncServer
 server: Server
 loop: Optional[asyncio.AbstractEventLoop] = None
@@ -47,6 +48,8 @@ page_routes: Dict[Callable, str] = {}
 
 startup_handlers: List[Union[Callable, Awaitable]] = []
 shutdown_handlers: List[Union[Callable, Awaitable]] = []
+connect_handlers: List[Union[Callable, Awaitable]] = []
+disconnect_handlers: List[Union[Callable, Awaitable]] = []
 
 
 def get_task_id() -> int:

+ 2 - 1
nicegui/helpers.py

@@ -1,5 +1,6 @@
 import asyncio
 import functools
+import inspect
 from contextlib import nullcontext
 from typing import Any, Awaitable, Callable, Optional, Union
 
@@ -22,7 +23,7 @@ def safe_invoke(func: Union[Callable, Awaitable], client: Optional[Client] = Non
             background_tasks.create(func_with_client())
         else:
             with client or nullcontext():
-                result = func()
+                result = func(client) if len(inspect.signature(func).parameters) == 1 and client is not None else func()
             if isinstance(result, Awaitable):
                 async def result_with_client():
                     with client or nullcontext():

+ 7 - 2
nicegui/nicegui.py

@@ -4,13 +4,14 @@ import urllib.parse
 from pathlib import Path
 from typing import Dict, Optional
 
-from fastapi import FastAPI, HTTPException, Request
+from fastapi import HTTPException, Request
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.responses import FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 
 from . import background_tasks, binding, globals
+from .app import App
 from .client import Client
 from .dependencies import js_components, js_dependencies
 from .element import Element
@@ -18,7 +19,7 @@ from .error import error_content
 from .helpers import safe_invoke
 from .page import page
 
-globals.app = app = FastAPI()
+globals.app = app = App()
 globals.sio = sio = SocketManager(app=app)._sio
 
 app.add_middleware(GZipMiddleware)
@@ -93,6 +94,8 @@ async def handle_handshake(sid: str) -> bool:
     sio.enter_room(sid, client.id)
     for t in client.connect_handlers:
         safe_invoke(t, client)
+    for t in globals.connect_handlers:
+        safe_invoke(t, client)
     return True
 
 
@@ -105,6 +108,8 @@ async def handle_disconnect(sid: str) -> None:
         delete_client(client.id)
     for t in client.disconnect_handlers:
         safe_invoke(t, client)
+    for t in globals.disconnect_handlers:
+        safe_invoke(t, client)
 
 
 @sio.on('event')

+ 1 - 1
nicegui/page.py

@@ -63,7 +63,7 @@ class page:
                         await AsyncUpdater(result)
                 task = background_tasks.create(wait_for_result())
                 deadline = time.time() + self.response_timeout
-                while task and not client.is_waiting_for_handshake and not task.done():
+                while task and not client.is_waiting_for_connection and not task.done():
                     if time.time() > deadline:
                         raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
                     await asyncio.sleep(0.1)

+ 0 - 2
nicegui/ui.py

@@ -46,10 +46,8 @@ from .elements.upload import Upload as upload
 from .elements.video import Video as video
 from .functions.html import add_body_html, add_head_html
 from .functions.javascript import run_javascript
-from .functions.lifecycle import on_connect, on_disconnect, on_shutdown, on_startup, shutdown
 from .functions.notify import notify
 from .functions.open import open
-from .functions.static_files import add_static_files
 from .functions.timer import Timer as timer
 from .functions.update import update
 from .page import page

+ 6 - 6
tests/test_auto_context.py

@@ -48,12 +48,12 @@ def test_adding_elements_with_async_await(screen: Screen):
     cB.find_element(By.XPATH, f'.//*[contains(text(), "B")]')
 
 
-def test_autoupdate_after_handshake(screen: Screen):
+def test_autoupdate_after_connected(screen: Screen):
     @ui.page('/')
     async def page(client: Client):
-        ui.label('before handshake')
-        await client.handshake()
-        ui.label('after handshake')
+        ui.label('before connected')
+        await client.connected()
+        ui.label('after connected')
         await asyncio.sleep(1)
         ui.label('one')
         await asyncio.sleep(1)
@@ -62,8 +62,8 @@ def test_autoupdate_after_handshake(screen: Screen):
         ui.label('three')
 
     screen.open('/')
-    screen.should_contain('before handshake')
-    screen.should_contain('after handshake')
+    screen.should_contain('before connected')
+    screen.should_contain('after connected')
     screen.should_not_contain('one')
     screen.wait_for('one')
     screen.should_not_contain('two')

+ 2 - 2
tests/test_javascript.py

@@ -24,7 +24,7 @@ def test_run_javascript_on_value_change(screen: Screen):
         async def set_title(e: ValueChangeEventArguments) -> None:
             await ui.run_javascript(f'document.title = "{e.value}"')
         ui.radio(['Page Title A', 'Page Title B'], on_change=set_title)
-        await client.handshake()
+        await client.connected()
         await ui.run_javascript('document.title = "Initial Page Title"')
 
     screen.open('/')
@@ -38,7 +38,7 @@ def test_run_javascript_on_value_change(screen: Screen):
     assert screen.selenium.title == 'Page Title A'
 
 
-def test_run_javascript_before_client_handshake(screen: Screen):
+def test_run_javascript_before_client_connected(screen: Screen):
     @ui.page('/')
     async def page():
         ui.label('before js')

+ 34 - 6
tests/test_lifecycle.py

@@ -1,21 +1,49 @@
-from nicegui import ui
+from typing import List
+
+from nicegui import Client, app, ui
 
 from .screen import Screen
 
 
-def test_adding_elements_during_onconnect(screen: Screen):
-    ui.label('Label 1')
-    ui.on_connect(lambda: ui.label('Label 2'))
+def test_adding_elements_during_onconnect_on_auto_index_page(screen: Screen):
+    connections = []
+    ui.label('Adding labels on_connect')
+    app.on_connect(lambda _: connections.append(ui.label(f'new connection {len(connections)}')))
 
     screen.open('/')
-    screen.should_contain('Label 2')
+    screen.should_contain('new connection 0')
+    screen.open('/')
+    screen.should_contain('new connection 0')
+    screen.should_contain('new connection 1')
+    screen.open('/')
+    screen.should_contain('new connection 0')
+    screen.should_contain('new connection 1')
+    screen.should_contain('new connection 2')
 
 
 def test_async_connect_handler(screen: Screen):
     async def run_js():
         result.text = await ui.run_javascript('41 + 1')
     result = ui.label()
-    ui.on_connect(run_js)
+    app.on_connect(run_js)
 
     screen.open('/')
     screen.should_contain('42')
+
+
+def test_connect_disconnect_is_called_for_each_client(screen: Screen):
+    events: List[str] = []
+
+    @ui.page('/')
+    def page(client: Client):
+        ui.label(f'client id: {client.id}')
+    app.on_connect(lambda: events.append('connect'))
+    app.on_disconnect(lambda: events.append('disconnect'))
+
+    screen.open('/')
+    screen.wait(0.5)
+    screen.open('/')
+    screen.wait(0.5)
+    screen.open('/')
+    screen.wait(0.5)
+    assert events == ['connect', 'disconnect', 'connect', 'disconnect', 'connect']

+ 24 - 9
tests/test_page.py

@@ -1,11 +1,9 @@
 import asyncio
 from uuid import uuid4
 
-import pytest
-
 from nicegui import Client, background_tasks, ui
 
-from .screen import PORT, Screen
+from .screen import Screen
 
 
 def test_page(screen: Screen):
@@ -100,7 +98,7 @@ def test_shared_and_private_pages(screen: Screen):
     assert uuid1 == uuid2
 
 
-def test_wait_for_handshake(screen: Screen):
+def test_wait_for_connected(screen: Screen):
     async def load() -> None:
         label.text = 'loading...'
         # NOTE we can not use asyncio.create_task() here because we are on a different thread than the NiceGUI event loop
@@ -114,18 +112,35 @@ def test_wait_for_handshake(screen: Screen):
     async def page(client: Client):
         global label
         label = ui.label()
-        await client.handshake()
+        await client.connected()
         await load()
 
     screen.open('/')
     screen.should_contain('delayed data has been loaded')
 
 
-def test_adding_elements_after_handshake(screen: Screen):
+def test_wait_for_disconnect(screen: Screen):
+    events = []
+
+    @ui.page('/')
+    async def page(client: Client):
+        await client.connected()
+        events.append('connected')
+        await client.disconnected()
+        events.append('disconnected')
+
+    screen.open('/')
+    screen.wait(0.1)
+    screen.open('/')
+    screen.wait(0.1)
+    assert events == ['connected', 'disconnected', 'connected']
+
+
+def test_adding_elements_after_connected(screen: Screen):
     @ui.page('/')
     async def page(client: Client):
         ui.label('before')
-        await client.handshake()
+        await client.connected()
         ui.label('after')
 
     screen.open('/')
@@ -144,10 +159,10 @@ def test_exception(screen: Screen):
     screen.assert_py_logger('ERROR', 'some exception')
 
 
-def test_exception_after_handshake(screen: Screen):
+def test_exception_after_connected(screen: Screen):
     @ui.page('/')
     async def page(client: Client):
-        await client.handshake()
+        await client.connected()
         ui.label('this is shown')
         raise Exception('some exception')
 

+ 27 - 26
website/reference.py

@@ -1,7 +1,7 @@
 import uuid
 from typing import Dict
 
-from nicegui import ui
+from nicegui import app, ui
 
 from .example import example
 
@@ -480,28 +480,27 @@ All three functions also provide `remove` and `replace` parameters in case the p
 
     h3('Action')
 
-    @example('''#### Lifecycle
+    @example('''#### Lifecycle functions
 
-You can run a function or coroutine as a parallel task by passing it to one of the following register methods:
+You can register coroutines or functions to be called for the following events:
 
-- `ui.on_startup`: Called when NiceGUI is started or restarted.
-- `ui.on_shutdown`: Called when NiceGUI is shut down or restarted.
-- `ui.on_connect`: Called when a client connects to NiceGUI. (Optional argument: Starlette request)
-- `ui.on_disconnect`: Called when a client disconnects from NiceGUI. (Optional argument: socket)
+- `app.on_startup`: called when NiceGUI is started or restarted
+- `app.on_shutdown`: called when NiceGUI is shut down or restarted
+- `app.on_connect`: called for each client which connects (optional argument: nicegui.Client)
+- `app.on_disconnect`: called for each client which disconnects (optional argument: nicegui.Client)
 
-When NiceGUI is shut down or restarted, the startup tasks will be automatically canceled.
-''', immediate=True)
+When NiceGUI is shut down or restarted, all tasks still in execution will be automatically canceled.
+''')
     def lifecycle_example():
-        import asyncio
-
-        l = ui.label()
+        from nicegui import app
 
-        async def countdown():
-            for i in [5, 4, 3, 2, 1, 0]:
-                l.text = f'{i}...' if i else 'Take-off!'
-                await asyncio.sleep(1)
+        def handle_connect():
+            if watch.value:
+                count.set_text(str(int(count.text or 0) + 1))
 
-        ui.on_connect(countdown)
+        watch = ui.checkbox('count new connections')
+        count = ui.label().classes('mt-8 self-center text-5xl')
+        app.on_connect(handle_connect)
 
     @example(ui.timer)
     def timer_example():
@@ -659,29 +658,29 @@ If the page function expects a `request` argument, the request object is automat
 
         ui.link('Say hi to Santa!', 'repeat/Ho! /3')
 
-    @example('''#### Wait for Handshake with Client
+    @example('''#### Wait for Client Connection
 
 To wait for a client connection, you can add a `client` argument to the decorated page function
-and await `client.handshake()`.
+and await `client.connected()`.
 All code below that statement is executed after the websocket connection between server and client has been established.
 
 For example, this allows you to run JavaScript commands; which is only possible with a client connection (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
 Also it is possible to do async stuff while the user already sees some content.
 ''')
-    def wait_for_handshake_example():
+    def wait_for_connected_example():
         import asyncio
 
         from nicegui import Client
 
-        @ui.page('/wait_for_handshake')
-        async def wait_for_handshake(client: Client):
+        @ui.page('/wait_for_connection')
+        async def wait_for_connection(client: Client):
             ui.label('This text is displayed immediately.')
-            await client.handshake()
+            await client.connected()
             await asyncio.sleep(2)
             ui.label('This text is displayed 2 seconds after the page has been fully loaded.')
             ui.label(f'The IP address {client.ip} was obtained from the websocket.')
 
-        ui.link('wait for handshake', wait_for_handshake)
+        ui.link('wait for connection', wait_for_connection)
 
     @example('''#### Page Layout
 
@@ -766,9 +765,11 @@ You can also set `respond=False` to send a command without waiting for a respons
 
     h3('Routes')
 
-    @example(ui.add_static_files)
+    @example(app.add_static_files)
     def add_static_files_example():
-        ui.add_static_files('/examples', 'examples')
+        from nicegui import app
+
+        app.add_static_files('/examples', 'examples')
         ui.label('Some NiceGUI Examples').classes('text-h5')
         ui.link('AI interface', '/examples/ai_interface/main.py')
         ui.link('Custom FastAPI app', '/examples/fastapi/main.py')