瀏覽代碼

Provide app.storage.client as a location to store volatile data which only matters for the current connection (#2820)

* Implemented app.storage.session which enables the user to store data in the current Client instance - which in practice means "per browser tab".

* Replaced Client.state by ObservableDict
Moved context import to top of the file

* Renamed app.storage.session to app.storage.client.

Adjusted documentation of app.storage.client.

* Exchanged quotes
Added app.storage.client clear test

* Added documentation for app.storage.client
Dropped implementation client.current_client for now
Added exceptions for calls to app.storage.client from auto index or w/o client connection.
Added tests for changes

* Removed imports, simplified client availability check

* Updated documentation
Removed client connection state checks and tests

* Removed connection test_clear from

* Removed random import, not required for demo anymore

* Resolved merging conflicts with tab extension

* Merge fix

* Merge fix

* minimal updates to documentation

* code review

* Removed line duplication

* improve clearing of client storage

* fix typo

* add overview table

* renaming

* review documentation

---------

Co-authored-by: Rodja Trappe <rodja@zauberzeug.com>
Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Michael Ikemann 1 年之前
父節點
當前提交
1428e6dbf0

+ 2 - 0
nicegui/client.py

@@ -20,6 +20,7 @@ from .element import Element
 from .favicon import get_favicon_url
 from .javascript_request import JavaScriptRequest
 from .logging import log
+from .observables import ObservableDict
 from .outbox import Outbox
 from .version import __version__
 
@@ -73,6 +74,7 @@ class Client:
         self._body_html = ''
 
         self.page = page
+        self.storage = ObservableDict()
 
         self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
         self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []

+ 19 - 0
nicegui/storage.py

@@ -17,6 +17,7 @@ from starlette.responses import Response
 
 from . import background_tasks, context, core, json, observables
 from .logging import log
+from .observables import ObservableDict
 
 request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)
 
@@ -156,6 +157,18 @@ class Storage:
         """General storage shared between all users that is persisted on the server (where NiceGUI is executed)."""
         return self._general
 
+    @property
+    def client(self) -> ObservableDict:
+        """A volatile storage that is only kept during the current connection to the client.
+
+        Like `app.storage.tab` data is unique per browser tab but is even more volatile as it is already discarded
+        when the connection to the client is lost through a page reload or a navigation.
+        """
+        if self._is_in_auto_index_context():
+            raise RuntimeError('app.storage.client can only be used with page builder functions '
+                               '(https://nicegui.io/documentation/page)')
+        return context.get_client().storage
+
     @property
     def tab(self) -> observables.ObservableDict:
         """A volatile storage that is only kept during the current tab session."""
@@ -183,6 +196,12 @@ class Storage:
         """Clears all storage."""
         self._general.clear()
         self._users.clear()
+        try:
+            client = context.get_client()
+        except RuntimeError:
+            pass  # no client, could be a pytest
+        else:
+            client.storage.clear()
         self._tabs.clear()
         for filepath in self.path.glob('storage-*.json'):
             filepath.unlink()

+ 37 - 0
tests/test_storage.py

@@ -1,5 +1,6 @@
 import asyncio
 from pathlib import Path
+import pytest
 
 import httpx
 
@@ -227,3 +228,39 @@ def test_clear_tab_storage(screen: Screen):
     screen.click('clear')
     screen.wait(0.5)
     assert not tab_storages
+
+
+def test_client_storage(screen: Screen):
+    def increment():
+        app.storage.client['counter'] = app.storage.client['counter'] + 1
+
+    @ui.page('/')
+    def page():
+        app.storage.client['counter'] = 123
+        ui.button('Increment').on_click(increment)
+        ui.label().bind_text(app.storage.client, 'counter')
+
+    screen.open('/')
+    screen.should_contain('123')
+    screen.click('Increment')
+    screen.wait_for('124')
+
+    screen.switch_to(1)
+    screen.open('/')
+    screen.should_contain('123')
+
+    screen.switch_to(0)
+    screen.should_contain('124')
+
+
+def test_clear_client_storage(screen: Screen):
+    with pytest.raises(RuntimeError):  # no context (auto index)
+        app.storage.client.clear()
+
+    @ui.page('/')
+    def page():
+        app.storage.client['counter'] = 123
+        app.storage.client.clear()
+        assert app.storage.client == {}
+
+    screen.open('/')

+ 46 - 3
website/documentation/content/storage_documentation.py

@@ -14,13 +14,20 @@ doc.title('Storage')
 
 @doc.demo('Storage', '''
     NiceGUI offers a straightforward mechanism for data persistence within your application. 
-    It features four built-in storage types:
+    It features five built-in storage types:
 
     - `app.storage.tab`:
         Stored server-side in memory, this dictionary is unique to each tab session and can hold arbitrary objects.
         Data will be lost when restarting the server until <https://github.com/zauberzeug/nicegui/discussions/2841> is implemented.
         This storage is only available within [page builder functions](/documentation/page) 
         and requires an established connection, obtainable via [`await client.connected()`](/documentation/page#wait_for_client_connection).
+    - `app.storage.client`:
+        Also stored server-side in memory, this dictionary is unique to each client connection and can hold arbitrary objects.
+        Data will be discarded when the page is reloaded or the user navigates to another page.
+        Unlike data stored in `app.storage.tab` which can be persisted on the server even for days, 
+        `app.storage.client` helps caching resource-hungry objects such as a streaming or database connection you need to keep alive 
+        for dynamic site updates but would like to discard as soon as the user leaves the page or closes the browser. 
+        This storage is only available within [page builder functions](/documentation/page).
     - `app.storage.user`:
         Stored server-side, each dictionary is associated with a unique identifier held in a browser session cookie.
         Unique to each user, this storage is accessible across all their browser tabs.
@@ -35,6 +42,16 @@ doc.title('Storage')
     The user storage and browser storage are only available within `page builder functions </documentation/page>`_
     because they are accessing the underlying `Request` object from FastAPI.
     Additionally these two types require the `storage_secret` parameter in`ui.run()` to encrypt the browser session cookie.
+    
+    | Storage type                | `tab`  | `client` | `user` | `general` | `browser` |
+    |-----------------------------|--------|----------|--------|-----------|-----------|
+    | Location                    | Server | Server   | Server | Server    | Browser   |
+    | Across tabs                 | No     | No       | Yes    | Yes       | Yes       |
+    | Across browsers             | No     | No       | No     | Yes       | No        |
+    | Across page reloads         | Yes    | No       | Yes    | Yes       | Yes       |
+    | Needs page builder function | Yes    | Yes      | Yes    | No        | Yes       |
+    | Needs client connection     | Yes    | No       | No     | No        | No        |
+    | Write only before response  | No     | No       | No     | No        | Yes       |
 ''')
 def storage_demo():
     from nicegui import app
@@ -99,11 +116,37 @@ def ui_state():
     It is also more secure to use such a volatile storage for scenarios like logging into a bank account or accessing a password manager.
 ''')
 def tab_storage():
-    from nicegui import app
+    from nicegui import app, Client
 
     # @ui.page('/')
-    # async def index(client):
+    # async def index(client: Client):
     #     await client.connected()
     with ui.column():  # HIDE
         app.storage.tab['count'] = app.storage.tab.get('count', 0) + 1
         ui.label(f'Tab reloaded {app.storage.tab["count"]} times')
+        ui.button('Reload page', on_click=ui.navigate.reload)
+
+
+@doc.demo('Short-term memory', '''
+    The goal of `app.storage.client` is to store data only for the duration of the current page visit.
+    In difference to data stored in `app.storage.tab`
+    - which is persisted between page changes and even browser restarts as long as the tab is kept open -
+    the data in `app.storage.client` will be discarded if the user closes the browser, reloads the page or navigates to another page.
+    This is beneficial for resource-hungry, intentionally short-lived or sensitive data.
+    An example is a database connection, which should be closed as soon as the user leaves the page.
+    Additionally, this storage useful if you want to return a page with default settings every time a user reloads.
+    Meanwhile, it keeps the data alive during in-page navigation.
+    This is also helpful when updating elements on the site at intervals, such as a live feed.
+''')
+def short_term_memory():
+    from nicegui import app
+
+    # @ui.page('/')
+    # async def index():
+    with ui.column():  # HIDE
+        cache = app.storage.client
+        cache['count'] = 0
+        ui.label().bind_text_from(cache, 'count', lambda n: f'Updated {n} times')
+        ui.button('Update content',
+                  on_click=lambda: cache.update(count=cache['count'] + 1))
+        ui.button('Reload page', on_click=ui.navigate.reload)

+ 3 - 1
website/documentation/rendering.py

@@ -43,7 +43,9 @@ def render_page(documentation: DocumentationPage, *, with_menu: bool = True) ->
                     element = ui.restructured_text(part.description.replace(':param ', ':'))
                 else:
                     element = ui.markdown(part.description)
-                element.classes('bold-links arrow-links rst-param-tables')
+                element.classes('bold-links arrow-links')
+                if ':param' in part.description:
+                    element.classes('rst-param-tables')
             if part.ui:
                 part.ui()
             if part.demo: