Skip to content

Commit ca3002c

Browse files
committed
Implementing localStorage integration
1 parent 3f804ca commit ca3002c

File tree

8 files changed

+105
-11
lines changed

8 files changed

+105
-11
lines changed

src/js/packages/@reactpy/client/src/components.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ export function Layout(props: { client: ReactPyClient }): JSX.Element {
4040
[currentModel, props.client],
4141
);
4242

43+
useEffect(
44+
() =>
45+
props.client.onMessage("sync-local-storage", ({ type, storage}) => {
46+
for (let itemKey in storage) {
47+
window.localStorage.setItem(
48+
itemKey,
49+
storage[itemKey]
50+
)
51+
}
52+
})
53+
)
54+
4355
return (
4456
<ClientContext.Provider value={props.client}>
4557
<Element model={currentModel} />

src/js/packages/@reactpy/client/src/messages.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export type LayoutEventMessage = {
1212
data: any;
1313
};
1414

15-
export type IncomingMessage = LayoutUpdateMessage;
16-
export type OutgoingMessage = LayoutEventMessage;
15+
export type LocalStorageUpdateMessage = {
16+
type: "sync-local-storage",
17+
storage: any;
18+
}
19+
20+
export type IncomingMessage = LayoutUpdateMessage | LocalStorageUpdateMessage;
21+
export type OutgoingMessage = LayoutEventMessage | LocalStorageUpdateMessage;
1722
export type Message = IncomingMessage | OutgoingMessage;

src/js/packages/@reactpy/client/src/reactpy-client.ts

+17
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,23 @@ function createReconnectingWebSocket(
204204
socket.current.onopen = () => {
205205
everConnected = true;
206206
logger.log("client connected");
207+
208+
let storage : any = {}
209+
for (let i = 0; i <= window.localStorage.length; i++) {
210+
let storage_key = window.localStorage.key(i)
211+
if (storage_key){
212+
storage[storage_key] = window.localStorage.getItem(storage_key)
213+
}
214+
socket.current?.send(
215+
JSON.stringify(
216+
{
217+
"type": "sync-local-storage",
218+
"storage": storage
219+
}
220+
)
221+
)
222+
}
223+
207224
interval = startInterval;
208225
retries = 0;
209226
if (props.onOpen) {

src/py/reactpy/reactpy/backend/hooks.py

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from reactpy.backend.types import Connection, Location
77
from reactpy.core.hooks import Context, create_context, use_context
8+
from reactpy.core.types import LocalStorage
89

910
# backend implementations should establish this context at the root of an app
1011
ConnectionContext: Context[Connection[Any] | None] = create_context(None)
@@ -27,3 +28,7 @@ def use_scope() -> MutableMapping[str, Any]:
2728
def use_location() -> Location:
2829
"""Get the current :class:`~reactpy.backend.types.Connection`'s location."""
2930
return use_connection().location
31+
32+
def use_local_storage() -> LocalStorage:
33+
"""Get the storage object for the connection"""
34+
return use_connection().storage

src/py/reactpy/reactpy/backend/starlette.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from reactpy.config import REACTPY_WEB_MODULES_DIR
3030
from reactpy.core.layout import Layout
3131
from reactpy.core.serve import RecvCoroutine, SendCoroutine, serve_layout
32-
from reactpy.core.types import RootComponentConstructor
32+
from reactpy.core.types import RootComponentConstructor, LocalStorage
3333

3434
logger = logging.getLogger(__name__)
3535

@@ -79,7 +79,7 @@ def use_connection() -> Connection[WebSocket]:
7979
if not isinstance(conn.carrier, WebSocket): # nocov
8080
msg = f"Connection has unexpected carrier {conn.carrier}. Are you running with a Flask server?"
8181
raise TypeError(msg)
82-
return conn
82+
return
8383

8484

8585
@dataclass
@@ -140,6 +140,7 @@ async def model_stream(socket: WebSocket) -> None:
140140
pathname = "/" + socket.scope["path_params"].get("path", "")
141141
pathname = pathname[len(options.url_prefix) :] or "/"
142142
search = socket.scope["query_string"].decode()
143+
storage_obj = LocalStorage(sock=socket)
143144

144145
try:
145146
await serve_layout(
@@ -149,10 +150,12 @@ async def model_stream(socket: WebSocket) -> None:
149150
value=Connection(
150151
scope=socket.scope,
151152
location=Location(pathname, f"?{search}" if search else ""),
153+
storage=storage_obj,
152154
carrier=socket,
153155
),
154156
)
155157
),
158+
storage_obj,
156159
send,
157160
recv,
158161
)

src/py/reactpy/reactpy/backend/types.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dataclasses import dataclass
66
from typing import Any, Callable, Generic, Protocol, TypeVar, runtime_checkable
77

8-
from reactpy.core.types import RootComponentConstructor
8+
from reactpy.core.types import RootComponentConstructor, LocalStorage
99

1010
_App = TypeVar("_App")
1111

@@ -51,6 +51,8 @@ class Connection(Generic[_Carrier]):
5151
location: Location
5252
"""The current location (URL)"""
5353

54+
storage: LocalStorage
55+
5456
carrier: _Carrier
5557
"""How the connection is mediated. For example, a request or websocket.
5658

src/py/reactpy/reactpy/core/serve.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from anyio.abc import TaskGroup
99

1010
from reactpy.config import REACTPY_DEBUG_MODE
11-
from reactpy.core.types import LayoutEventMessage, LayoutType, LayoutUpdateMessage
12-
11+
from reactpy.core.types import StorageEventMessage, LocalStorage, LayoutEventMessage, LayoutType, LayoutUpdateMessage
12+
from reactpy.core.layout import Layout
1313
logger = getLogger(__name__)
1414

1515

@@ -33,6 +33,7 @@ class Stop(BaseException):
3333

3434
async def serve_layout(
3535
layout: LayoutType[LayoutUpdateMessage, LayoutEventMessage],
36+
storage: LocalStorage,
3637
send: SendCoroutine,
3738
recv: RecvCoroutine,
3839
) -> None:
@@ -41,11 +42,10 @@ async def serve_layout(
4142
try:
4243
async with create_task_group() as task_group:
4344
task_group.start_soon(_single_outgoing_loop, layout, send)
44-
task_group.start_soon(_single_incoming_loop, task_group, layout, recv)
45+
task_group.start_soon(_single_incoming_loop, task_group, layout, storage, recv)
4546
except Stop:
4647
logger.info(f"Stopped serving {layout}")
4748

48-
4949
async def _single_outgoing_loop(
5050
layout: LayoutType[LayoutUpdateMessage, LayoutEventMessage], send: SendCoroutine
5151
) -> None:
@@ -63,13 +63,23 @@ async def _single_outgoing_loop(
6363
logger.error(msg)
6464
raise
6565

66+
async def incoming_router(
67+
layout: Layout,
68+
storage: LocalStorage,
69+
event: LayoutEventMessage or StorageEventMessage,
70+
):
71+
if event["type"] == "sync-local-storage":
72+
storage._sync(event["storage"])
73+
else:
74+
await layout.deliver(event)
6675

6776
async def _single_incoming_loop(
6877
task_group: TaskGroup,
6978
layout: LayoutType[LayoutUpdateMessage, LayoutEventMessage],
79+
storage: LocalStorage,
7080
recv: RecvCoroutine,
7181
) -> None:
7282
while True:
7383
# We need to fire and forget here so that we avoid waiting on the completion
7484
# of this event handler before receiving and running the next one.
75-
task_group.start_soon(layout.deliver, await recv())
85+
task_group.start_soon(incoming_router, layout, storage, await recv())

src/py/reactpy/reactpy/core/types.py

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

3-
import sys
3+
import sys, json
4+
from starlette.websockets import WebSocket
45
from collections import namedtuple
56
from collections.abc import Mapping, Sequence
67
from types import TracebackType
@@ -233,3 +234,42 @@ class LayoutEventMessage(TypedDict):
233234
"""The ID of the event handler."""
234235
data: Sequence[Any]
235236
"""A list of event data passed to the event handler."""
237+
238+
class StorageEventMessage(TypedDict):
239+
type: Literal["sync-local-storage"]
240+
storage: dict
241+
242+
class LocalStorage():
243+
_socket: WebSocket
244+
storage: dict
245+
246+
def __init__(self, sock):
247+
self._socket = sock
248+
self.storage = {}
249+
250+
def _sync(self, sto):
251+
self.storage = sto
252+
253+
async def _sync_client(self):
254+
await self._socket.send_text(
255+
json.dumps(
256+
{
257+
"type": "sync-local-storage",
258+
"storage": self.storage
259+
}
260+
)
261+
)
262+
263+
def get_item(
264+
self,
265+
key: str
266+
):
267+
return self.storage.get(key)
268+
269+
async def set_item(
270+
self,
271+
key: str,
272+
value: str
273+
):
274+
self.storage[key] = value
275+
await self._sync_client()

0 commit comments

Comments
 (0)