Skip to content
Merged
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
3 changes: 1 addition & 2 deletions src/js/packages/@reactpy/client/src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
30 changes: 26 additions & 4 deletions src/js/packages/@reactpy/client/src/reactpy-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 = () => { };
Expand All @@ -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<ReactPyModule>;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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';

Expand All @@ -229,7 +247,7 @@ export class SimpleReactPyClient
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
z-index: 100000;
`;

pipeContainer.style.cssText = `
Expand Down Expand Up @@ -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)");
Expand All @@ -344,7 +366,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();
}
Expand Down
10 changes: 0 additions & 10 deletions src/py/reactpy/reactpy/backend/sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 22 additions & 3 deletions src/py/reactpy/reactpy/core/state_recovery.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import base64
from dataclasses import asdict, is_dataclass
import datetime
import hashlib
import time
Expand Down Expand Up @@ -58,14 +59,16 @@ def __init__(
datetime.datetime,
datetime.date,
datetime.time,
datetime.timezone,
datetime.timedelta,
]
)

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
Expand Down Expand Up @@ -132,8 +135,13 @@ 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._deserializer_map = deserializer_map or {}
self._provided_default_serializer = default_serializer
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
Expand Down Expand Up @@ -253,6 +261,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:
Expand Down
2 changes: 1 addition & 1 deletion src/py/reactpy/reactpy/core/vdom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down