diff --git a/nicegui/client.py b/nicegui/client.py index dbe153d7d..00b3610e7 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -86,11 +86,6 @@ def is_auto_index_client(self) -> bool: """Return True if this client is the auto-index client.""" return self is self.auto_index_client - @staticmethod - def current_client() -> Optional[Client]: - """Returns the current client if obtainable from the current context.""" - return get_client() - @property def ip(self) -> Optional[str]: """Return the IP address of the client, or None if the client is not connected.""" diff --git a/nicegui/storage.py b/nicegui/storage.py index 67ae594f2..8241c6f33 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -153,23 +153,24 @@ def general(self) -> Dict: @property def client(self) -> ObservableDict: - """Client storage that is persisted on the server (where NiceGUI is executed) on a per client - connection basis. - - The data is lost when the client disconnects through reloading the page, closing the tab or - navigating away from the page. It can be used to store data that is only relevant for the current view such - as filter settings on a dashboard or in-page navigation. As the data is not persisted it also allows the - storage of data structures such as database connections, pandas tables, numpy arrays, user specific ML models - or other living objects that are not serializable to JSON. - """ + """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)') client = context.get_client() + if not client.has_socket_connection: + raise RuntimeError('app.storage.client can only be used with a client connection; ' + 'see https://nicegui.io/documentation/page#wait_for_client_connection to await it') return client.state def clear(self) -> None: """Clears all storage.""" self._general.clear() self._users.clear() - if get_slot_stack(): + if get_slot_stack() and not self._is_in_auto_index_context(): self.client.clear() for filepath in self.path.glob('storage-*.json'): filepath.unlink() diff --git a/tests/test_client_state.py b/tests/test_client_state.py index 77e53cb42..3b04e56b9 100644 --- a/tests/test_client_state.py +++ b/tests/test_client_state.py @@ -1,23 +1,51 @@ -from nicegui import ui, app +import pytest + +from nicegui import ui, app, context from nicegui.testing import Screen def test_session_state(screen: Screen): - app.storage.client['counter'] = 123 - def increment(): app.storage.client['counter'] = app.storage.client['counter'] + 1 - ui.button('Increment').on_click(increment) - ui.label().bind_text(app.storage.client, 'counter') + @ui.page('/') + async def page(): + await context.get_client().connected() + app.storage.client['counter'] = 123 + ui.button('Increment').on_click(increment) + ui.label().bind_text(app.storage.client, 'counter') + 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_no_connection(screen: Screen): + @ui.page('/') + async def page(): + with pytest.raises(RuntimeError): # no connection yet + app.storage.client['counter'] = 123 + + screen.open('/') def test_clear(screen: Screen): - app.storage.client['counter'] = 123 - app.storage.client.clear() - assert 'counter' not in app.storage.client + with pytest.raises(RuntimeError): # no context (auto index) + app.storage.client.clear() + + @ui.page('/') + async def page(): + await context.get_client().connected() + app.storage.client['counter'] = 123 + app.storage.client.clear() + assert 'counter' not in app.storage.client + + screen.open('/') diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 792776910..4d2a2833f 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -1,3 +1,4 @@ +import random from collections import Counter from datetime import datetime @@ -8,7 +9,6 @@ counter = Counter() # type: ignore start = datetime.now().strftime(r'%H:%M, %d %B %Y') - doc.title('Storage') @@ -16,6 +16,12 @@ NiceGUI offers a straightforward method for data persistence within your application. It features three built-in storage types: + - `app.storage.client`: + Stored server-side in memory, this dictionary is unique to each client connection and can hold arbitrary + objects. + Data will be lost when the connection is closed due to a page reload or navigation to another page. + 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.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. @@ -86,3 +92,47 @@ def ui_state(): # .classes('w-full').bind_value(app.storage.user, 'note') # END OF DEMO ui.textarea('This note is kept between visits').classes('w-full').bind_value(app.storage.user, 'note') + + +@doc.demo('Short-term memory', ''' + The goal of `app.storage.client` is to store data only for the duration of the current page visit and + only as long as the client is connected. + 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 may be beneficial for resource hungry or intentionally very short lived data such as a database + connection which should be closed as soon as the user leaves the page, sensitive data or if you on purpose + want to return a page with the default settings every time the user reloads the page while keeping the + data alive during in-page navigation or when updating elements on the site in intervals such as a live feed. + ''') +def tab_storage(): + from nicegui import app, context + + class DbConnection: # dummy class to simulate a database connection + def __init__(self): + self.connection_id = random.randint(0, 9999) + + def status(self) -> str: + return random.choice(['healthy', 'unhealthy']) + + def get_db_connection(): # per-client db connection + cs = app.storage.client + if 'connection' in cs: + return cs['connection'] + cs['connection'] = DbConnection() + return cs['connection'] + + # @ui.page('/') + # async def index(client): + # await client.connected() + with ui.row(): # HIDE + status = ui.markdown('DB status') + def update_status(): + db_con = get_db_connection() + status.set_content('**Database connection ID**: ' + f'{db_con.connection_id}\n\n' + f'**Status**: {db_con.status()}') + with ui.row(): + ui.button('Refresh', on_click=update_status) + ui.button("Reload page", on_click=lambda: ui.navigate.reload()) + update_status()