From ee12f37129e671ae7da251a64b9439695dd59dda Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 03:35:53 +0000 Subject: [PATCH 1/8] Delete commented out code --- src/py/reactpy/reactpy/backend/sanic.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/py/reactpy/reactpy/backend/sanic.py b/src/py/reactpy/reactpy/backend/sanic.py index bad90b072..e648747fa 100644 --- a/src/py/reactpy/reactpy/backend/sanic.py +++ b/src/py/reactpy/reactpy/backend/sanic.py @@ -193,16 +193,6 @@ async def model_stream( ), constructor, ) - # await serve_layout( - # Layout( - # ConnectionContext( - # constructor(), - # value=, - # ) - # ), - # send, - # recv, - # ) api_blueprint.add_websocket_route( model_stream, From 416f4c7ee5434fb9d55efe85c718c931dec04f21 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 03:42:17 +0000 Subject: [PATCH 2/8] add serialization for timezone and timedelta --- src/py/reactpy/reactpy/core/state_recovery.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/py/reactpy/reactpy/core/state_recovery.py b/src/py/reactpy/reactpy/core/state_recovery.py index 38be33786..7a427da7b 100644 --- a/src/py/reactpy/reactpy/core/state_recovery.py +++ b/src/py/reactpy/reactpy/core/state_recovery.py @@ -1,5 +1,6 @@ import asyncio import base64 +from dataclasses import asdict, is_dataclass import datetime import hashlib import time @@ -58,6 +59,8 @@ def __init__( datetime.datetime, datetime.date, datetime.time, + datetime.timezone, + datetime.timedelta, ] ) @@ -132,7 +135,7 @@ def __init__( self._type_id_to_object = type_id_to_object self._max_object_length = max_object_length self._max_num_state_objects = max_num_state_objects - self._default_serializer = default_serializer + self._provided_default_serializer = default_serializer self._deserializer_map = deserializer_map or {} def _get_otp_code(self, target_time: float) -> str: @@ -253,6 +256,17 @@ def _sign_serialization( def _serialize_object(self, obj: Any) -> bytes: return orjson.dumps(obj, default=self._default_serializer) + def _default_serializer(self, obj: Any) -> bytes: + if isinstance(obj, datetime.timezone): + return {"name": obj.tzname(None), "offset": obj.utcoffset(None)} + if isinstance(obj, datetime.timedelta): + return {"days": obj.days, "seconds": obj.seconds, "microseconds": obj.microseconds} + if is_dataclass(obj): + return asdict(obj) + if self._provided_default_serializer: + return self._provided_default_serializer(obj) + raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable") + def _do_deserialize( self, typ: type, result: Any, custom_deserializer: Callable | None ) -> Any: From e8236794c784e54658239256b8dc7d6dc518b8c9 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 03:45:02 +0000 Subject: [PATCH 3/8] don't show reconnecting layer on first attempt --- src/js/packages/@reactpy/client/src/reactpy-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/packages/@reactpy/client/src/reactpy-client.ts b/src/js/packages/@reactpy/client/src/reactpy-client.ts index c5018e9a5..7380c2008 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-client.ts +++ b/src/js/packages/@reactpy/client/src/reactpy-client.ts @@ -344,7 +344,7 @@ export class SimpleReactPyClient this.shouldReconnect = true; window.setTimeout(() => { - if (!this.didReconnectingCallback && this.reconnectingCallback) { + if (!this.didReconnectingCallback && this.reconnectingCallback && maxRetries != connectionAttemptsRemaining) { this.didReconnectingCallback = true; this.reconnectingCallback(); } From f61be8467f99ef24b84045614e7471fb3e69db3e Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 04:03:47 +0000 Subject: [PATCH 4/8] Only connect after layout update handlers are set up --- .../@reactpy/client/src/components.tsx | 3 +-- .../@reactpy/client/src/reactpy-client.ts | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/js/packages/@reactpy/client/src/components.tsx b/src/js/packages/@reactpy/client/src/components.tsx index fd23d3a8a..0f1c1722d 100644 --- a/src/js/packages/@reactpy/client/src/components.tsx +++ b/src/js/packages/@reactpy/client/src/components.tsx @@ -29,13 +29,12 @@ export function Layout(props: { client: ReactPyClient }): JSX.Element { useEffect( () => - props.client.onMessage("layout-update", ({ path, model, state_vars }) => { + props.client.onLayoutUpdate((path: string, model: any) => { if (path === "") { Object.assign(currentModel, model); } else { setJsonPointer(currentModel, path, model); } - props.client.updateStateVars(state_vars); forceUpdate(); }), [currentModel, props.client], diff --git a/src/js/packages/@reactpy/client/src/reactpy-client.ts b/src/js/packages/@reactpy/client/src/reactpy-client.ts index 7380c2008..81ce3ca04 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-client.ts +++ b/src/js/packages/@reactpy/client/src/reactpy-client.ts @@ -16,6 +16,8 @@ export interface ReactPyClient { */ onMessage(type: string, handler: (message: any) => void): () => void; + onLayoutUpdate(handler: (path: string, model: any) => void): void; + /** * Send a message to the server. * @@ -43,6 +45,7 @@ export abstract class BaseReactPyClient implements ReactPyClient { private resolveReady: (value: undefined) => void; protected stateVars: object; protected debugMessages: boolean; + protected layoutUpdateHandlers: Array<(path: string, model: any) => void> = []; constructor() { this.resolveReady = () => { }; @@ -59,6 +62,10 @@ export abstract class BaseReactPyClient implements ReactPyClient { }; } + onLayoutUpdate(handler: (path: string, model: any) => void): void { + this.layoutUpdateHandlers.push(handler); + } + abstract sendMessage(message: any): void; abstract loadModule(moduleName: string): Promise; @@ -146,7 +153,8 @@ enum messageTypes { isReady = "is-ready", reconnectingCheck = "reconnecting-check", clientState = "client-state", - stateUpdate = "state-update" + stateUpdate = "state-update", + layoutUpdate = "layout-update", }; export class SimpleReactPyClient @@ -198,6 +206,10 @@ export class SimpleReactPyClient this.onMessage(messageTypes.isReady, (msg) => { this.isReady = true; this.salt = msg.salt; }); this.onMessage(messageTypes.clientState, () => { this.sendClientState() }); this.onMessage(messageTypes.stateUpdate, (msg) => { this.updateClientState(msg.state_vars) }); + this.onMessage(messageTypes.layoutUpdate, (msg) => { + this.updateClientState(msg.state_vars); + this.invokeLayoutUpdateHandlers(msg.path, msg.model); + }) this.reconnect() @@ -211,7 +223,13 @@ export class SimpleReactPyClient window.addEventListener('scroll', reconnectOnUserAction); } - showReconnectingGrayout() { + protected invokeLayoutUpdateHandlers(path: string, model: any) { + this.layoutUpdateHandlers.forEach(func => { + func(path, model); + }); + } + + protected showReconnectingGrayout() { const overlay = document.createElement('div'); overlay.id = 'reactpy-reconnect-overlay'; @@ -328,6 +346,10 @@ export class SimpleReactPyClient const maxInterval = this.reconnectOptions?.maxInterval || 20000; const maxRetries = this.reconnectOptions?.maxRetries || 20; + if (this.layoutUpdateHandlers.length == 0) { + setTimeout(() => { this.reconnect(onOpen, interval, connectionAttemptsRemaining, lastAttempt); }, 10); + return + } if (connectionAttemptsRemaining <= 0) { logger.warn("Giving up on reconnecting (hit retry limit)"); From 6c6273339f5ce1d55886749aa8e1a7135db2b87b Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 04:04:02 +0000 Subject: [PATCH 5/8] perf tweak that apparently was never saved --- src/py/reactpy/reactpy/core/vdom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/reactpy/reactpy/core/vdom.py b/src/py/reactpy/reactpy/core/vdom.py index e494b5269..2bb3120dd 100644 --- a/src/py/reactpy/reactpy/core/vdom.py +++ b/src/py/reactpy/reactpy/core/vdom.py @@ -328,7 +328,7 @@ def _validate_child_key_integrity(value: Any) -> None: ) else: for child in value: - if isinstance(child, ComponentType) and child.key is None: + if child.key is None and isinstance(child, ComponentType): warn(f"Key not specified for child in list {child}", UserWarning) elif isinstance(child, Mapping) and "key" not in child: # remove 'children' to reduce log spam From a5f6790ef5b60dee1eb8c825c629bec56b2a9424 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 04:10:26 +0000 Subject: [PATCH 6/8] deserialization as well for timezone --- src/py/reactpy/reactpy/core/state_recovery.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/py/reactpy/reactpy/core/state_recovery.py b/src/py/reactpy/reactpy/core/state_recovery.py index 7a427da7b..a9caa64bc 100644 --- a/src/py/reactpy/reactpy/core/state_recovery.py +++ b/src/py/reactpy/reactpy/core/state_recovery.py @@ -136,7 +136,12 @@ def __init__( self._max_object_length = max_object_length self._max_num_state_objects = max_num_state_objects self._provided_default_serializer = default_serializer - self._deserializer_map = deserializer_map or {} + deserialization_map = { + datetime.timezone: lambda x: datetime.timezone( + datetime.timedelta(**x["offset"]), x["name"] + ), + } + self._deserializer_map = deserialization_map | (deserializer_map or {}) def _get_otp_code(self, target_time: float) -> str: at = self._totp.at From 3374cce0285ba348e2199110eb40e4f33132af8b Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 04:12:22 +0000 Subject: [PATCH 7/8] timezone and timedelta --- src/py/reactpy/reactpy/core/state_recovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/reactpy/reactpy/core/state_recovery.py b/src/py/reactpy/reactpy/core/state_recovery.py index a9caa64bc..6979de7ff 100644 --- a/src/py/reactpy/reactpy/core/state_recovery.py +++ b/src/py/reactpy/reactpy/core/state_recovery.py @@ -68,7 +68,7 @@ def _map_objects_to_ids(self, serializable_types: Iterable[type]) -> dict: self._object_to_type_id = {} self._type_id_to_object = {} for idx, typ in enumerate( - (None, bool, str, int, float, list, tuple, UUID, *serializable_types) + (None, bool, str, int, float, list, tuple, UUID, datetime.timezone, datetime.timedelta, *serializable_types) ): idx_as_bytes = str(idx).encode("utf-8") self._object_to_type_id[typ] = idx_as_bytes From de0f2d06a791c0e1d511c75ad42efdd5bed74a79 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Tue, 5 Mar 2024 04:15:43 +0000 Subject: [PATCH 8/8] alter z-index --- src/js/packages/@reactpy/client/src/reactpy-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/packages/@reactpy/client/src/reactpy-client.ts b/src/js/packages/@reactpy/client/src/reactpy-client.ts index 81ce3ca04..c69db96bc 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-client.ts +++ b/src/js/packages/@reactpy/client/src/reactpy-client.ts @@ -247,7 +247,7 @@ export class SimpleReactPyClient display: flex; justify-content: center; align-items: center; - z-index: 1000; + z-index: 100000; `; pipeContainer.style.cssText = `