Skip to content

Commit

Permalink
feat: implement local storage
Browse files Browse the repository at this point in the history
* feat: propagate local storage into session to keep it into sync
  • Loading branch information
FabienArcellier committed Dec 23, 2024
1 parent 4548f29 commit d6d7982
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 12 deletions.
9 changes: 3 additions & 6 deletions src/ui/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ export function generateCore() {
setActivePageFromKey(pageKey);
});
addMailSubscription("localStorageSetItem", (event) => {
localStorage.setItem("wf." + event.key, JSON.stringify(event.value));
localStorage.setItem(event.key, event.value);
});
addMailSubscription("localStorageRemoveItem", (event) => {
localStorage.removeItem("wf." + event.key);
localStorage.removeItem(event.key);
});
sendKeepAliveMessage();
if (mode.value != "edit") return;
Expand All @@ -78,10 +78,7 @@ export function generateCore() {
const localStorageItems = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith("wf.")) {
const value = localStorage.getItem(key);
localStorageItems[key.replace("wf.", "")] = JSON.parse(value);
}
localStorageItems[key] = localStorage.getItem(key);
}

const response = await fetch("./api/init", {
Expand Down
1 change: 0 additions & 1 deletion src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ def _handle_event(self, session: WriterSession, event: WriterEvent) -> EventResp
import traceback as tb

result = session.event_handler.handle(event)

mutations = {}

try:
Expand Down
36 changes: 34 additions & 2 deletions src/writer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ class WriterSession:
"""

def __init__(self, session_id: str, cookies: Optional[Dict[str, str]], headers: Optional[Dict[str, str]], local_storage: Optional[Dict[str, Any]]) -> None:
if local_storage is None:
local_storage = {}

self.session_id = session_id
self.cookies = cookies
self.headers = headers
Expand Down Expand Up @@ -1079,17 +1082,46 @@ def call_frontend_function(self, module_key: str, function_name: str, args: List
"args": args
})

def local_storage_set_item(self, key: str, value: Any) -> None:
def local_storage_set_item(self, key: str, value: str) -> None:
"""
Saves a value to the browser's local storage.
>>> state.local_storage_set_item("my_key", "value")
The value must be a string. If it is another type, it will be converted to a character string without serialization.
The browser's local storage values as text.
It is recommended to serialize the data upstream, for example in JSON.
>>> state.local_storage_set_item("my_key", json.dumps({"value": 1}))
"""
if not isinstance(value, str):
value = str(value)

self.add_mail("localStorageSetItem", {
"key": key,
"value": value
})

def local_storage_remove_item(self, key: str, value: Any) -> None:
_session = get_session()
assert _session is not None, "local_storage_set_item must be used within a user request."
_session.local_storage[key] = value

def local_storage_remove_item(self, key: str) -> None:
"""
Removes a value from the browser's local storage.
>>> state.local_storage_remove_item("my_key")
"""
self.add_mail("localStorageRemoveItem", {
"key": key
})

_session = get_session()
assert _session is not None, "local_storage_remove_item must be used within a user request."
if key in _session.local_storage:
del _session.local_storage[key]

class MiddlewareExecutor():
"""
A MiddlewareExecutor executes middleware in a controlled context. It allows writer framework
Expand Down
24 changes: 23 additions & 1 deletion tests/backend/fixtures/app_runner_fixtures.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import contextlib
from typing import Optional

from writer.app_runner import AppRunner
from writer.ss_types import InitSessionRequestPayload
from writer.core import session_manager, use_request_context
from writer.ss_types import AppProcessServerRequest, InitSessionRequestPayload

FIXED_SESSION_ID = "0000000000000000000000000000000000000000000000000000000000000000" # Compliant session number

Expand Down Expand Up @@ -32,3 +34,23 @@ async def init_app_session(app_runner: AppRunner,
result = await app_runner.init_session(init_session_payload)

return result.payload.model_dump().get("sessionId")


@contextlib.contextmanager
def within_message_request(session_id=FIXED_SESSION_ID, request: AppProcessServerRequest=None):
"""
This fixture starts a session and emulates the context of an event message.
>>> with within_message_request():
>>> _session = core.get_session()
>>> _session.local_storage['key'] = "value"
:param session_id: the session identifier
:param request: the request to create
"""
if request is None:
request = AppProcessServerRequest(type="event")

session_manager.get_new_session(proposed_session_id=session_id)
with use_request_context(session_id=session_id, request=request):
yield
40 changes: 38 additions & 2 deletions tests/backend/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import pyarrow as pa
import pytest
import writer as wf
from writer import audit_and_fix, wf_project
from writer import audit_and_fix, core, wf_project
from writer.core import (
BytesWrapper,
EventDeserialiser,
Expand All @@ -31,6 +31,7 @@
)
from writer.ss_types import WriterEvent

from backend.fixtures import app_runner_fixtures
from tests.backend import test_app_dir
from tests.backend.fixtures import (
writer_fixtures,
Expand Down Expand Up @@ -155,7 +156,7 @@ def test_apply_mutation_marker(self) -> None:
'+name': 'Robert',
'+state\\.with\\.dots': None,
'+utfࠀ': 23,
'+a\.b': 3
r'+a\.b': 3
}

self.sp_simple_dict.apply_mutation_marker()
Expand Down Expand Up @@ -751,6 +752,40 @@ def test_unpickable_members(self) -> None:
json.dumps(cloned.user_state.to_dict())
json.dumps(cloned.mail)

def test_local_storage_set_item_should_update_backend_localstorage(self):
"""
Tests that setting an item in the local storage updates the backend local storage
"""
with app_runner_fixtures.within_message_request():
# Assign
_state = WriterState({"a": 1, "b": 2})

# Acts
_state.local_storage_set_item("tab", "tab1")

# Assert
_session = core.get_session()
assert _session.local_storage['tab'] == "tab1"
assert _state.mail[0].get("type") == "localStorageSetItem"
assert _state.mail[0].get("payload") == {"key": "tab", "value": "tab1"}

def test_local_storage_set_item_should_set_value_as_string(self):
"""
Tests that using local_storage_set_item forces the value to be a string.
"""
with app_runner_fixtures.within_message_request():
# Assign
_state = WriterState({"a": 1, "b": 2})

# Acts
_state.local_storage_set_item("tab", 0)

# Assert
_session = core.get_session()
assert _session.local_storage['tab'] == "0"
assert _state.mail[0].get("type") == "localStorageSetItem"
assert _state.mail[0].get("payload") == {"key": "tab", "value": "0"}


class TestEventDeserialiser:

Expand Down Expand Up @@ -1222,6 +1257,7 @@ def test_get_new_session_proposed(self) -> None:
self.sm.get_new_session(
{"testCookie": "yes"},
{"origin": "example.com"},
{},
self.proposed_session_id
)
self.sm.get_session(self.proposed_session_id)
Expand Down

0 comments on commit d6d7982

Please sign in to comment.