Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from . import binding, elements, html, run, storage, ui
from .api_router import APIRouter
from .app.app import App
from .cache import cache
from .client import Client
from .context import context
from .element_filter import ElementFilter
Expand All @@ -17,6 +18,7 @@
'__version__',
'app',
'binding',
'cache',
'context',
'elements',
'html',
Expand Down
60 changes: 60 additions & 0 deletions nicegui/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import hashlib
from typing import Dict, List, Set, TypeVar, Union, overload

from . import json


class Cached:
"""A base class for cached objects."""


class CachedStr(Cached, str):
"""A string that is marked as cached."""


class CachedList(Cached, list):
"""A list that is marked as cached."""


class CachedDict(Cached, dict):
"""A dict that is marked as cached."""


@overload
def cache(x: str) -> CachedStr: ...


@overload
def cache(x: List) -> CachedList: ...


@overload
def cache(x: Dict) -> CachedDict: ...


def cache(x):
"""Mark an object as cached by converting it to a cached subtype."""
if isinstance(x, str):
return CachedStr(x)
if isinstance(x, list):
return CachedList(x)
if isinstance(x, dict):
return CachedDict(x)
raise ValueError(f'Unsupported type: {type(x)}')


T = TypeVar('T')


def add_hash(obj: T, known_hashes: Set[str]) -> Union[T, str]:
"""Serialize an object to a JSON-compatible format."""
if isinstance(obj, Cached):
serialized = json.dumps(obj)
hash_id = hashlib.sha256(serialized.encode()).hexdigest()[:32]
if hash_id in known_hashes:
return f'CACHE_{hash_id}'
else:
known_hashes.add(hash_id)
return f'CACHE_{hash_id}_{serialized}'
else:
return obj
17 changes: 16 additions & 1 deletion nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@
from collections import defaultdict
from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Any, Awaitable, Callable, ClassVar, Dict, Iterable, Iterator, List, Optional, Union
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
ClassVar,
Dict,
Iterable,
Iterator,
List,
Optional,
Set,
Union,
)

from fastapi import Request
from fastapi.responses import Response
Expand Down Expand Up @@ -56,6 +69,7 @@ def __init__(self, page: page, *, request: Optional[Request]) -> None:

self.elements: Dict[int, Element] = {}
self.next_element_id: int = 0
self.known_hashes: Set[str] = set()
self._waiting_for_connection: asyncio.Event = asyncio.Event()
self.is_waiting_for_connection: bool = False
self.is_waiting_for_disconnect: bool = False
Expand Down Expand Up @@ -128,6 +142,7 @@ def build_response(self, request: Request, status_code: int = 200) -> Response:
"""Build a FastAPI response for the client."""
self.outbox.updates.clear()
prefix = request.headers.get('X-Forwarded-Prefix', request.scope.get('root_path', ''))
self.known_hashes.union(json.loads(request.cookies.get('__nicegui_hash_keys__', '[]')))
elements = json.dumps({
id: element._to_dict() for id, element in self.elements.items() # pylint: disable=protected-access
})
Expand Down
5 changes: 3 additions & 2 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from . import core, events, helpers, json, storage
from .awaitable_response import AwaitableResponse, NullResponse
from .cache import add_hash
from .classes import Classes
from .context import context
from .dependencies import (
Expand Down Expand Up @@ -209,13 +210,13 @@ def _collect_slot_dict(self) -> Dict[str, Any]:
def _to_dict(self) -> Dict[str, Any]:
return {
'tag': self.tag,
**({'text': self._text} if self._text is not None else {}),
**({'text': add_hash(self._text, self.client.known_hashes)} if self._text is not None else {}),
**{
key: value
for key, value in {
'class': self._classes,
'style': self._style,
'props': self._props,
'props': {key: add_hash(value, self.client.known_hashes) for key, value in self._props.items()},
'slots': self._collect_slot_dict(),
'children': [child.id for child in self.default_slot.children],
'events': [listener.to_dict() for listener in self._event_listeners.values()],
Expand Down
22 changes: 21 additions & 1 deletion nicegui/static/nicegui.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ function stringifyEventArgs(args, event_args) {
return result;
}

const dataStore = JSON.parse(localStorage.getItem("__nicegui_data_store__") || "{}");

function unhash(value) {
if (typeof value === "string" && value.startsWith("CACHE_")) {
const hashId = value.split("_")[1];
if (hashId in dataStore) return dataStore[hashId];
console.log("Cache miss:", hashId);
const jsonStart = "CACHE_".length + hashId.length + 1; // +1 for the underscore
const result = JSON.parse(value.substring(jsonStart));
dataStore[hashId] = result;
localStorage.setItem("__nicegui_data_store__", JSON.stringify(dataStore));
const cookie_value = JSON.stringify(Object.keys(dataStore));
const cookie_expiration = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
document.cookie = `__nicegui_hash_keys__=${cookie_value};expires=${cookie_expiration.toUTCString()};path=/`;
return result;
}
return value;
}

const waitingCallbacks = new Map();
function throttle(callback, time, leading, trailing, id) {
if (time <= 0) {
Expand Down Expand Up @@ -159,6 +178,7 @@ function renderRecursively(elements, id) {
style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, "") || undefined,
...element.props,
};
Object.entries(props).forEach(([key, value]) => (props[key] = unhash(value)));
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith(":")) {
try {
Expand Down Expand Up @@ -233,7 +253,7 @@ function renderRecursively(elements, id) {
}
const children = data.ids.map((id) => renderRecursively(elements, id));
if (name === "default" && element.text !== null) {
children.unshift(element.text);
children.unshift(unhash(element.text));
}
return [...rendered, ...children];
};
Expand Down