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

Access to client's localStorage and sessionStorage #1091

Closed
3 changes: 2 additions & 1 deletion docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.
Unreleased
----------

Nothing yet...
**Added**
- :issue:`1075` - Add support to localStorage and sessionStorage access


v1.0.2
Expand Down
24 changes: 24 additions & 0 deletions src/js/packages/@reactpy/client/src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ export function Layout(props: { client: ReactPyClient }): JSX.Element {
[currentModel, props.client],
);

useEffect(
() =>
props.client.onMessage("sync-local-storage", ({ type, storage}) => {
for (let itemKey in storage) {
window.localStorage.setItem(
itemKey,
storage[itemKey]
)
}
})
)

useEffect(
() =>
props.client.onMessage("sync-session-storage", ({ type, storage}) => {
for (let itemKey in storage) {
window.sessionStorage.setItem(
itemKey,
storage[itemKey]
)
}
})
)

return (
<ClientContext.Provider value={props.client}>
<Element model={currentModel} />
Expand Down
14 changes: 12 additions & 2 deletions src/js/packages/@reactpy/client/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export type LayoutEventMessage = {
data: any;
};

export type IncomingMessage = LayoutUpdateMessage;
export type OutgoingMessage = LayoutEventMessage;
export type LocalStorageUpdateMessage = {
type: "sync-local-storage",
storage: any;
}

export type SessionStorageUpdateMessage = {
type: "sync-session-storage",
storage: any;
}

export type IncomingMessage = LayoutUpdateMessage | LocalStorageUpdateMessage | SessionStorageUpdateMessage;
export type OutgoingMessage = LayoutEventMessage | LocalStorageUpdateMessage | SessionStorageUpdateMessage;
export type Message = IncomingMessage | OutgoingMessage;
33 changes: 33 additions & 0 deletions src/js/packages/@reactpy/client/src/reactpy-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,39 @@ function createReconnectingWebSocket(
socket.current.onopen = () => {
everConnected = true;
logger.log("client connected");

let _localStorage : any = {}
for (let i = 0; i <= window.localStorage.length; i++) {
let storage_key = window.localStorage.key(i)
if (storage_key){
_localStorage[storage_key] = window.localStorage.getItem(storage_key)
}
}
socket.current?.send(
JSON.stringify(
{
"type": "sync-local-storage",
"storage": _localStorage
}
)
)

let _sessionStorage : any = {}
for (let i = 0; i <= window.sessionStorage.length; i++) {
let storage_key = window.sessionStorage.key(i)
if (storage_key){
_sessionStorage[storage_key] = window.sessionStorage.getItem(storage_key)
}
}
socket.current?.send(
JSON.stringify(
{
"type": "sync-local-storage",
"storage": _sessionStorage
}
)
)

interval = startInterval;
retries = 0;
if (props.onOpen) {
Expand Down
9 changes: 9 additions & 0 deletions src/py/reactpy/reactpy/backend/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from reactpy.backend.types import Connection, Location
from reactpy.core.hooks import Context, create_context, use_context
from reactpy.core.types import LocalStorage, SessionStorage

# backend implementations should establish this context at the root of an app
ConnectionContext: Context[Connection[Any] | None] = create_context(None)
Expand All @@ -27,3 +28,11 @@ def use_scope() -> MutableMapping[str, Any]:
def use_location() -> Location:
"""Get the current :class:`~reactpy.backend.types.Connection`'s location."""
return use_connection().location

def use_local_storage() -> LocalStorage:
"""Get the localStorage object for the connection"""
return use_connection().local_storage

def use_session_storage() -> SessionStorage:
"""Get the sessionStorage object for the connection"""
return use_connection().session_storage
8 changes: 7 additions & 1 deletion src/py/reactpy/reactpy/backend/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from reactpy.config import REACTPY_WEB_MODULES_DIR
from reactpy.core.layout import Layout
from reactpy.core.serve import RecvCoroutine, SendCoroutine, serve_layout
from reactpy.core.types import RootComponentConstructor
from reactpy.core.types import RootComponentConstructor, LocalStorage, SessionStorage

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -140,6 +140,8 @@ async def model_stream(socket: WebSocket) -> None:
pathname = "/" + socket.scope["path_params"].get("path", "")
pathname = pathname[len(options.url_prefix) :] or "/"
search = socket.scope["query_string"].decode()
local_storage_obj = LocalStorage(sock=socket)
session_storage_obj = SessionStorage(sock=socket)

try:
await serve_layout(
Expand All @@ -149,10 +151,14 @@ async def model_stream(socket: WebSocket) -> None:
value=Connection(
scope=socket.scope,
location=Location(pathname, f"?{search}" if search else ""),
local_storage=local_storage_obj,
session_storage=session_storage_obj,
carrier=socket,
),
)
),
local_storage_obj,
session_storage_obj,
send,
recv,
)
Expand Down
8 changes: 7 additions & 1 deletion src/py/reactpy/reactpy/backend/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass
from typing import Any, Callable, Generic, Protocol, TypeVar, runtime_checkable

from reactpy.core.types import RootComponentConstructor
from reactpy.core.types import RootComponentConstructor, LocalStorage, SessionStorage

_App = TypeVar("_App")

Expand Down Expand Up @@ -51,6 +51,12 @@ class Connection(Generic[_Carrier]):
location: Location
"""The current location (URL)"""

local_storage: LocalStorage
"""An object to obtain client localStorage"""

session_storage: SessionStorage
"""An object to obtain client sessionStorage"""

carrier: _Carrier
"""How the connection is mediated. For example, a request or websocket.

Expand Down
33 changes: 28 additions & 5 deletions src/py/reactpy/reactpy/core/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@
from anyio.abc import TaskGroup

from reactpy.config import REACTPY_DEBUG_MODE
from reactpy.core.types import LayoutEventMessage, LayoutType, LayoutUpdateMessage

from reactpy.core.types import (
LocalStorageEventMessage,
SessionStorageEventMessage,
LocalStorage,
SessionStorage,
LayoutEventMessage,
LayoutType,
LayoutUpdateMessage
)
from reactpy.core.layout import Layout
logger = getLogger(__name__)


Expand All @@ -33,6 +41,8 @@ class Stop(BaseException):

async def serve_layout(
layout: LayoutType[LayoutUpdateMessage, LayoutEventMessage],
local_storage: LocalStorage,
session_storage: SessionStorage,
send: SendCoroutine,
recv: RecvCoroutine,
) -> None:
Expand All @@ -41,11 +51,10 @@ async def serve_layout(
try:
async with create_task_group() as task_group:
task_group.start_soon(_single_outgoing_loop, layout, send)
task_group.start_soon(_single_incoming_loop, task_group, layout, recv)
task_group.start_soon(_single_incoming_loop, task_group, layout, local_storage, session_storage,recv)
except Stop:
logger.info(f"Stopped serving {layout}")


async def _single_outgoing_loop(
layout: LayoutType[LayoutUpdateMessage, LayoutEventMessage], send: SendCoroutine
) -> None:
Expand All @@ -63,13 +72,27 @@ async def _single_outgoing_loop(
logger.error(msg)
raise

async def incoming_router(
layout: Layout,
local_storage: LocalStorage,
session_storage: SessionStorage,
event: LayoutEventMessage or LocalStorageEventMessage or SessionStorageEventMessage,
):
if event["type"] == "sync-local-storage":
local_storage._sync(event["storage"])
elif event["type"] == "sync-session-storage":
session_storage._sync(event["storage"])
elif event["type"] == "layout-event":
await layout.deliver(event)

async def _single_incoming_loop(
task_group: TaskGroup,
layout: LayoutType[LayoutUpdateMessage, LayoutEventMessage],
local_storage: LocalStorage,
session_storage: SessionStorage,
recv: RecvCoroutine,
) -> None:
while True:
# We need to fire and forget here so that we avoid waiting on the completion
# of this event handler before receiving and running the next one.
task_group.start_soon(layout.deliver, await recv())
task_group.start_soon(incoming_router, layout, local_storage, session_storage, await recv())
89 changes: 88 additions & 1 deletion src/py/reactpy/reactpy/core/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import sys
import sys, json
from starlette.websockets import WebSocket
from collections import namedtuple
from collections.abc import Mapping, Sequence
from types import TracebackType
Expand Down Expand Up @@ -233,3 +234,89 @@ class LayoutEventMessage(TypedDict):
"""The ID of the event handler."""
data: Sequence[Any]
"""A list of event data passed to the event handler."""

class LocalStorageEventMessage(TypedDict):
"""Message describing an event containing localStorage"""

type: Literal["sync-local-storage"]
"""The type of message"""
storage: dict
"""A dictionary containing localStorage items"""

class SessionStorageEventMessage(TypedDict):
"""Message describing an event containing sessionStorage"""

type: Literal["sync-session-storage"]
"""Message describing an event containing localStorage"""
storage: dict
"""A dictionary containing localStorage items"""

class LocalStorage():
_socket: WebSocket
storage: dict

def __init__(self, sock):
self._socket = sock
self.storage = {}

def _sync(self, sto):
self.storage = sto

async def _sync_client(self):
await self._socket.send_text(
json.dumps(
{
"type": "sync-local-storage",
"storage": self.storage
}
)
)

def get_item(
self,
key: str
):
return self.storage.get(key)

async def set_item(
self,
key: str,
value: str
):
self.storage[key] = value
await self._sync_client()

class SessionStorage():
_socket: WebSocket
storage: dict

def __init__(self, sock):
self._socket = sock
self.storage = {}

def _sync(self, sto):
self.storage = sto

async def _sync_client(self):
await self._socket.send_text(
json.dumps(
{
"type": "sync-session-storage",
"storage": self.storage
}
)
)

def get_item(
self,
key: str
):
return self.storage.get(key)

async def set_item(
self,
key: str,
value: str
):
self.storage[key] = value
await self._sync_client()
Loading