Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 29 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
db6b065
Implemented app.storage.session which enables the user to store data …
Alyxion Apr 4, 2024
d9403b4
Merge branch 'main' into feature/per_session_data
Alyxion Apr 4, 2024
3fedd36
Replaced Client.state by ObservableDict
Alyxion Apr 5, 2024
8fc9208
Merge branch 'main' into feature/per_session_data
Alyxion Apr 5, 2024
ea8dad5
Renamed app.storage.session to app.storage.client.
Alyxion Apr 6, 2024
e627934
Merge remote-tracking branch 'origin/feature/per_session_data' into f…
Alyxion Apr 6, 2024
df3335a
Merge branch 'main' into feature/per_session_data
Alyxion Apr 6, 2024
9dd4227
Exchanged quotes
Alyxion Apr 6, 2024
ca99fcf
Merge remote-tracking branch 'origin/feature/per_session_data' into f…
Alyxion Apr 6, 2024
08d73bd
Added documentation for app.storage.client
Alyxion Apr 7, 2024
06d6393
Removed imports, simplified client availability check
Alyxion Apr 7, 2024
bb6f44b
Merge branch 'main' into feature/per_session_data
Alyxion Apr 7, 2024
040ebb8
Updated documentation
Alyxion Apr 8, 2024
8013ea0
Merge remote-tracking branch 'origin/feature/per_session_data' into f…
Alyxion Apr 8, 2024
f02d9e9
Removed connection test_clear from
Alyxion Apr 8, 2024
c57a93f
Removed random import, not required for demo anymore
Alyxion Apr 8, 2024
0714c51
Merge remote-tracking branch 'nicegui/main' into feature/per_session_…
Alyxion Apr 8, 2024
ac4ecb1
Resolved merging conflicts with tab extension
Alyxion Apr 8, 2024
edca313
Merge fix
Alyxion Apr 8, 2024
6e85268
Merge fix
Alyxion Apr 8, 2024
bad5cec
minimal updates to documentation
rodja Apr 8, 2024
c01d0d1
code review
falkoschindler Apr 8, 2024
26ba191
Removed line duplication
Alyxion Apr 8, 2024
9ade1f9
improve clearing of client storage
falkoschindler Apr 9, 2024
44f70c7
Merge branch 'feature/per_session_data' of github.com:Alyxion/nicegui…
falkoschindler Apr 9, 2024
6563825
fix typo
falkoschindler Apr 9, 2024
ff58c03
add overview table
falkoschindler Apr 9, 2024
7f856b4
renaming
falkoschindler Apr 9, 2024
0f72368
review documentation
falkoschindler Apr 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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__

Expand Down Expand Up @@ -73,6 +74,7 @@ def __init__(self, page: page, *, shared: bool = False) -> None:
self._body_html = ''

self.page = page
self.state = ObservableDict()

self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
Expand Down
16 changes: 16 additions & 0 deletions nicegui/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
from starlette.responses import Response

from . import background_tasks, context, core, json, observables
from .context import get_slot_stack
from .logging import log
from .observables import ObservableDict

request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)

Expand Down Expand Up @@ -156,6 +158,18 @@ def general(self) -> PersistentDict:
"""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)')
client = context.get_client()
Alyxion marked this conversation as resolved.
Show resolved Hide resolved
return client.state

@property
def tab(self) -> observables.ObservableDict:
"""A volatile storage that is only kept during the current tab session."""
Expand Down Expand Up @@ -183,6 +197,8 @@ def clear(self) -> None:
"""Clears all storage."""
self._general.clear()
self._users.clear()
if not self._is_in_auto_index_context() and get_slot_stack():
self.client.clear()
rodja marked this conversation as resolved.
Show resolved Hide resolved
self._tabs.clear()
for filepath in self.path.glob('storage-*.json'):
filepath.unlink()
Expand Down
40 changes: 40 additions & 0 deletions tests/test_client_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest

from nicegui import ui, app, context
from nicegui.testing import Screen


def test_session_state(screen: Screen):
def increment():
app.storage.client['counter'] = app.storage.client['counter'] + 1

@ui.page('/')
async def page():
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_clear(screen: Screen):
with pytest.raises(RuntimeError): # no context (auto index)
app.storage.client.clear()

@ui.page('/')
async def page():
app.storage.client['counter'] = 123
app.storage.client.clear()
assert 'counter' not in app.storage.client

screen.open('/')
35 changes: 34 additions & 1 deletion website/documentation/content/storage_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@

@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.
Expand Down Expand Up @@ -107,3 +114,29 @@ def tab_storage():
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=lambda: 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 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 short_term_memory():
from nicegui import app

# @ui.page('/')
# async def index(client):
with ui.column(): # HIDE
cache = app.storage.client
cache['counter'] = 0
ui.label().bind_text_from(cache, 'counter',
backward=lambda n: f'Content updated {n} times')
ui.button('Update content',
on_click=lambda: cache.update({"counter": cache["counter"] + 1}))
ui.button("Reload page", on_click=lambda: ui.navigate.reload())