From 6d378c10497366fe95cf4581165ef0826c2dd225 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:01:03 +0000 Subject: [PATCH 01/12] added support for hot reloading via jurigged --- .../@reactpy/client/src/reactpy-client.ts | 27 ++++++-- src/py/reactpy/reactpy/backend/sanic.py | 7 +- src/py/reactpy/reactpy/core/layout.py | 14 ++-- src/py/reactpy/reactpy/core/serve.py | 65 +++++++++++++++---- src/py/reactpy/reactpy/core/types.py | 6 ++ src/py/reactpy/reactpy/hot_reloading.py | 29 +++++++++ 6 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 src/py/reactpy/reactpy/hot_reloading.py diff --git a/src/js/packages/@reactpy/client/src/reactpy-client.ts b/src/js/packages/@reactpy/client/src/reactpy-client.ts index b840479a0..a934308ef 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-client.ts +++ b/src/js/packages/@reactpy/client/src/reactpy-client.ts @@ -1,5 +1,5 @@ -import { ReactPyModule } from "./reactpy-vdom"; import logger from "./logger"; +import { ReactPyModule } from "./reactpy-vdom"; /** * A client for communicating with a ReactPy server. @@ -108,6 +108,7 @@ export type SimpleReactPyClientProps = { connectionTimeout?: number; debugMessages?: boolean; socketLoopThrottle?: number; + pingInterval?: number; }; /** @@ -156,6 +157,7 @@ enum messageTypes { clientState = "client-state", stateUpdate = "state-update", layoutUpdate = "layout-update", + pingIntervalSet = "ping-interval-set", }; export class SimpleReactPyClient @@ -180,6 +182,8 @@ export class SimpleReactPyClient private didReconnectingCallback: boolean; private willReconnect: boolean; private socketLoopThrottle: number; + private pingPongIntervalId?: number | null; + private pingInterval: number; constructor(props: SimpleReactPyClientProps) { super(); @@ -193,6 +197,7 @@ export class SimpleReactPyClient ); this.idleDisconnectTimeMillis = (props.idleDisconnectTimeSeconds || 240) * 1000; this.connectionTimeout = props.connectionTimeout || 5000; + this.pingInterval = props.pingInterval || 0; this.lastActivityTime = Date.now() this.reconnectOptions = props.reconnectOptions this.debugMessages = props.debugMessages || false; @@ -215,8 +220,9 @@ export class SimpleReactPyClient this.updateClientState(msg.state_vars); this.invokeLayoutUpdateHandlers(msg.path, msg.model); this.willReconnect = true; // don't indicate a reconnect until at least one successful layout update - }) - + }); + this.onMessage(messageTypes.pingIntervalSet, (msg) => { this.pingInterval = msg.ping_interval; this.updatePingInterval(); }); + this.updatePingInterval() this.reconnect() const handleUserAction = (ev: any) => { @@ -350,11 +356,20 @@ export class SimpleReactPyClient } } + updatePingInterval(): void { + if (this.pingPongIntervalId) { + window.clearInterval(this.pingPongIntervalId); + } + if (this.pingInterval) { + this.pingPongIntervalId = window.setInterval(() => { this.socket.current?.readyState === WebSocket.OPEN && this.socket.current?.send("ping") }, this.pingInterval); + } + } + reconnect(onOpen?: () => void, interval: number = 750, connectionAttemptsRemaining: number = 20, lastAttempt: number = 0): void { const intervalJitter = this.reconnectOptions?.intervalJitter || 0.5; const backoffRate = this.reconnectOptions?.backoffRate || 1.2; - const maxInterval = this.reconnectOptions?.maxInterval || 20000; - const maxRetries = this.reconnectOptions?.maxRetries || 20; + const maxInterval = this.reconnectOptions?.maxInterval || 500; + const maxRetries = this.reconnectOptions?.maxRetries || 40; if (this.layoutUpdateHandlers.length == 0) { setTimeout(() => { this.reconnect(onOpen, interval, connectionAttemptsRemaining, lastAttempt); }, 10); @@ -412,6 +427,8 @@ export class SimpleReactPyClient clearInterval(this.socketLoopIntervalId); if (this.idleCheckIntervalId) clearInterval(this.idleCheckIntervalId); + if (this.pingPongIntervalId) + clearInterval(this.pingPongIntervalId); if (!this.sleeping) { const thisInterval = nextInterval(addJitter(interval, intervalJitter), backoffRate, maxInterval); const newRetriesRemaining = connectionAttemptsRemaining - 1; diff --git a/src/py/reactpy/reactpy/backend/sanic.py b/src/py/reactpy/reactpy/backend/sanic.py index e648747fa..0018c97c3 100644 --- a/src/py/reactpy/reactpy/backend/sanic.py +++ b/src/py/reactpy/reactpy/backend/sanic.py @@ -7,8 +7,8 @@ from typing import Any from urllib import parse as urllib_parse from uuid import uuid4 -import orjson +import orjson from sanic import Blueprint, Sanic, request, response from sanic.config import Config from sanic.server.websockets.connection import WebSocketConnection @@ -213,7 +213,10 @@ async def sock_send(value: Any) -> None: await socket.send(orjson.dumps(value).decode("utf-8")) async def sock_recv() -> Any: - data = await socket.recv() + while True: + data = await socket.recv() + if data != "ping": + break if data is None: raise Stop() return orjson.loads(data) diff --git a/src/py/reactpy/reactpy/core/layout.py b/src/py/reactpy/reactpy/core/layout.py index f2274c6a8..2c4ada19c 100644 --- a/src/py/reactpy/reactpy/core/layout.py +++ b/src/py/reactpy/reactpy/core/layout.py @@ -7,7 +7,6 @@ FIRST_COMPLETED, CancelledError, PriorityQueue, - Queue, Task, create_task, get_running_loop, @@ -19,9 +18,8 @@ from logging import getLogger from typing import ( Any, - Awaitable, + AsyncIterable, Callable, - Coroutine, Generic, NamedTuple, NewType, @@ -57,7 +55,6 @@ Key, LayoutEventMessage, LayoutUpdateMessage, - StateUpdateMessage, VdomChild, VdomDict, VdomJson, @@ -155,7 +152,10 @@ async def finish(self) -> None: del self._root_life_cycle_state_id del self._model_states_by_life_cycle_state_id - clear_hook_state(self._hook_state_token) + try: + clear_hook_state(self._hook_state_token) + except LookupError: + pass def start_rendering(self) -> None: self._schedule_render_task(self._root_life_cycle_state_id) @@ -188,7 +188,7 @@ async def render(self) -> LayoutUpdateMessage: else: # nocov return await self._serial_render() - async def render_until_queue_empty(self) -> None: + async def render_until_queue_empty(self) -> AsyncIterable[LayoutUpdateMessage]: model_state_id = await self._rendering_queue.get() while True: try: @@ -199,7 +199,7 @@ async def render_until_queue_empty(self) -> None: f"{model_state_id!r} - component already unmounted" ) else: - await self._create_layout_update(model_state, get_hook_state()) + yield await self._create_layout_update(model_state, get_hook_state()) # this might seem counterintuitive. What's happening is that events can get kicked off # and currently there's no (obvious) visibility on if we're waiting for them to finish # so this will wait up to 0.15 * 5 = 750 ms to see if any renders come in before diff --git a/src/py/reactpy/reactpy/core/serve.py b/src/py/reactpy/reactpy/core/serve.py index 85799c762..a4d091694 100644 --- a/src/py/reactpy/reactpy/core/serve.py +++ b/src/py/reactpy/reactpy/core/serve.py @@ -4,6 +4,7 @@ import string from collections.abc import Awaitable from logging import getLogger +from os import environ from typing import Callable from warnings import warn @@ -22,12 +23,26 @@ LayoutEventMessage, LayoutType, LayoutUpdateMessage, + PingIntervalSetMessage, ReconnectingCheckMessage, RootComponentConstructor, ) logger = getLogger(__name__) +MAX_HOT_RELOADING = environ.get("MAX_HOT_RELOADING", "0") in ( + "1", + "true", + "True", + "yes", +) +if MAX_HOT_RELOADING: + logger.warning("Doing maximum hot reloading") + from reactpy.hot_reloading import ( + monkeypatch_jurigged_to_kill_connections_if_function_update, + ) + + monkeypatch_jurigged_to_kill_connections_if_function_update() SendCoroutine = Callable[ [ @@ -128,24 +143,35 @@ def __init__( async def handle_connection( self, connection: Connection, constructor: RootComponentConstructor ): + if MAX_HOT_RELOADING: + from reactpy.hot_reloading import active_connections + + active_connections.append(connection) layout = Layout( ConnectionContext( constructor(), value=connection, ), ) - async with layout: - await self._handshake(layout) - # salt may be set to client's old salt during handshake - if self._state_recovery_manager: - layout.set_recovery_serializer( - self._state_recovery_manager.create_serializer(self._salt) + try: + async with layout: + await self._handshake(layout) + # salt may be set to client's old salt during handshake + if self._state_recovery_manager: + layout.set_recovery_serializer( + self._state_recovery_manager.create_serializer(self._salt) + ) + await serve_layout( + layout, + self._send, + self._recv, ) - await serve_layout( - layout, - self._send, - self._recv, - ) + finally: + if MAX_HOT_RELOADING: + try: + active_connections.remove(connection) + except ValueError: + pass async def _handshake(self, layout: Layout) -> None: await self._send(ReconnectingCheckMessage(type="reconnecting-check")) @@ -172,8 +198,22 @@ async def _handshake(self, layout: Layout) -> None: await self._indicate_ready(), async def _indicate_ready(self) -> None: + if MAX_HOT_RELOADING: + await self._send( + PingIntervalSetMessage(type="ping-interval-set", ping_interval=250) + ) await self._send(IsReadyMessage(type="is-ready", salt=self._salt)) + if MAX_HOT_RELOADING: + + async def _handle_rebuild_msg(self, msg: LayoutUpdateMessage) -> None: + await self._send(msg) + + else: + + async def _handle_rebuild_msg(self, msg: LayoutUpdateMessage) -> None: + pass # do nothing + async def _do_state_rebuild_for_reconnection(self, layout: Layout) -> str: salt = self._salt await self._send(ClientStateMessage(type="client-state")) @@ -197,7 +237,8 @@ async def _do_state_rebuild_for_reconnection(self, layout: Layout) -> str: salt = client_state_msg["salt"] layout.start_rendering_for_reconnect() - await layout.render_until_queue_empty() + async for msg in layout.render_until_queue_empty(): + await self._handle_rebuild_msg(msg) except StateRecoveryFailureError: logger.warning( "State recovery failed (likely client from different version). Starting fresh" diff --git a/src/py/reactpy/reactpy/core/types.py b/src/py/reactpy/reactpy/core/types.py index a4be74f61..b2b070b24 100644 --- a/src/py/reactpy/reactpy/core/types.py +++ b/src/py/reactpy/reactpy/core/types.py @@ -263,6 +263,12 @@ class LayoutEventMessage(TypedDict): """A list of event data passed to the event handler.""" +class PingIntervalSetMessage(TypedDict): + type: Literal["ping-interval-set"] + + ping_interval: int + + class Context(Protocol[_Type]): """Returns a :class:`ContextProvider` component""" diff --git a/src/py/reactpy/reactpy/hot_reloading.py b/src/py/reactpy/reactpy/hot_reloading.py new file mode 100644 index 000000000..6aae079a9 --- /dev/null +++ b/src/py/reactpy/reactpy/hot_reloading.py @@ -0,0 +1,29 @@ +import asyncio +import logging + +logger = logging.getLogger(__name__) + +active_connections = [] + + +def monkeypatch_jurigged_to_kill_connections_if_function_update(): + import jurigged.codetools as jurigged_codetools # type: ignore + + OrigFunctionDefinition = jurigged_codetools.FunctionDefinition + + class NewFunctionDefinition(OrigFunctionDefinition): + def reevaluate(self, new_node, glb): + if active_connections: + logger.info("Killing active connections") + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + tasks = [ + connection.carrier.websocket.close() + for connection in active_connections + ] + loop.run_until_complete(asyncio.gather(*tasks)) + loop.close() + active_connections.clear() + return super().reevaluate(new_node, glb) + + jurigged_codetools.FunctionDefinition = NewFunctionDefinition From eb6832e587b54fec45ef2790dae33c734f4da48b Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:07:41 +0000 Subject: [PATCH 02/12] rename env var --- src/py/reactpy/reactpy/core/serve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/reactpy/reactpy/core/serve.py b/src/py/reactpy/reactpy/core/serve.py index a4d091694..0485304da 100644 --- a/src/py/reactpy/reactpy/core/serve.py +++ b/src/py/reactpy/reactpy/core/serve.py @@ -30,7 +30,7 @@ logger = getLogger(__name__) -MAX_HOT_RELOADING = environ.get("MAX_HOT_RELOADING", "0") in ( +MAX_HOT_RELOADING = environ.get("REACTPY_MAX_HOT_RELOADING", "0") in ( "1", "true", "True", From ffbd5e7aaeaa56323b6f4528404c1d6fa0509a6b Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Wed, 17 Apr 2024 18:47:04 +0000 Subject: [PATCH 03/12] add id to event elements --- src/js/packages/event-to-object/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/packages/event-to-object/src/index.ts b/src/js/packages/event-to-object/src/index.ts index 9a40a2128..fe1442cef 100644 --- a/src/js/packages/event-to-object/src/index.ts +++ b/src/js/packages/event-to-object/src/index.ts @@ -276,6 +276,7 @@ function convertElement(element: EventTarget | HTMLElement | null): any { const convertGenericElement = (element: HTMLElement) => ({ tagName: element.tagName, boundingClientRect: { ...element.getBoundingClientRect() }, + id: element.id, }); const convertMediaElement = (element: HTMLMediaElement) => ({ From c4367ff96de76249ff60b7ae83ba0d5843876713 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Thu, 18 Apr 2024 04:05:21 +0000 Subject: [PATCH 04/12] Add jurigged decorator workaround --- src/py/reactpy/reactpy/hot_reloading.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/py/reactpy/reactpy/hot_reloading.py b/src/py/reactpy/reactpy/hot_reloading.py index 6aae079a9..38c33cf01 100644 --- a/src/py/reactpy/reactpy/hot_reloading.py +++ b/src/py/reactpy/reactpy/hot_reloading.py @@ -8,6 +8,7 @@ def monkeypatch_jurigged_to_kill_connections_if_function_update(): import jurigged.codetools as jurigged_codetools # type: ignore + import jurigged.utils as jurigged_utils # type: ignore OrigFunctionDefinition = jurigged_codetools.FunctionDefinition @@ -26,4 +27,12 @@ def reevaluate(self, new_node, glb): active_connections.clear() return super().reevaluate(new_node, glb) + def stash(self, lineno=1, col_offset=0): + if not isinstance(self.parent, OrigFunctionDefinition): + co = self.get_object() + if co and (delta := lineno - self.node.extent.lineno): + self.recode(jurigged_utils.shift_lineno(co, delta), use_cache=False) + + return super(OrigFunctionDefinition, self).stash(lineno, col_offset) + jurigged_codetools.FunctionDefinition = NewFunctionDefinition From 0830d5605816df28f2a56f63f3d060657acbc95f Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Thu, 25 Apr 2024 00:02:18 +0000 Subject: [PATCH 05/12] Fix input elements missing checked for checkboxes --- src/js/packages/event-to-object/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/packages/event-to-object/src/index.ts b/src/js/packages/event-to-object/src/index.ts index fe1442cef..e6e9f9ab5 100644 --- a/src/js/packages/event-to-object/src/index.ts +++ b/src/js/packages/event-to-object/src/index.ts @@ -304,7 +304,7 @@ const elementConverters: { [key: string]: (element: any) => any } = { FORM: (element: HTMLFormElement) => ({ elements: Array.from(element.elements).map(convertElement), }), - INPUT: (element: HTMLInputElement) => ({ value: element.value }), + INPUT: (element: HTMLInputElement) => ({ value: element.value, checked: element.checked }), METER: (element: HTMLMeterElement) => ({ value: element.value }), OPTION: (element: HTMLOptionElement) => ({ value: element.value }), OUTPUT: (element: HTMLOutputElement) => ({ value: element.value }), From 908b066ae4baa1fdd0edd20e1ee80a7db68417aa Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Fri, 3 May 2024 23:08:10 +0000 Subject: [PATCH 06/12] Fix effects running on reconnect anyways --- src/py/reactpy/reactpy/core/hooks.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/py/reactpy/reactpy/core/hooks.py b/src/py/reactpy/reactpy/core/hooks.py index 95fefaf94..5e4e6b450 100644 --- a/src/py/reactpy/reactpy/core/hooks.py +++ b/src/py/reactpy/reactpy/core/hooks.py @@ -54,7 +54,9 @@ class ReconnectingOnly(list): @overload -def use_state(initial_value: Callable[[], _Type], *, server_only: bool = False) -> State[_Type]: ... +def use_state( + initial_value: Callable[[], _Type], *, server_only: bool = False +) -> State[_Type]: ... @overload @@ -190,12 +192,9 @@ def use_effect( hook = get_current_hook() if hook.reconnecting.current: if not isinstance(dependencies, ReconnectingOnly): - return - dependencies = None - else: - if isinstance(dependencies, ReconnectingOnly): - return - dependencies = _try_to_infer_closure_values(function, dependencies) + return use_memo(lambda: None) + elif isinstance(dependencies, ReconnectingOnly): + return def add_effect(function: _EffectApplyFunc) -> None: if not asyncio.iscoroutinefunction(function): From 662f8a78e1f3907c5a6b3198d0c52c8f32218f8b Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Fri, 3 May 2024 23:16:45 +0000 Subject: [PATCH 07/12] fix --- src/py/reactpy/reactpy/core/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/reactpy/reactpy/core/hooks.py b/src/py/reactpy/reactpy/core/hooks.py index 5e4e6b450..7279b0191 100644 --- a/src/py/reactpy/reactpy/core/hooks.py +++ b/src/py/reactpy/reactpy/core/hooks.py @@ -192,7 +192,7 @@ def use_effect( hook = get_current_hook() if hook.reconnecting.current: if not isinstance(dependencies, ReconnectingOnly): - return use_memo(lambda: None) + return memoize(lambda: None) elif isinstance(dependencies, ReconnectingOnly): return From ce0783f5796a0d1e56f093944318d55099e254b6 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Fri, 17 May 2024 23:54:28 +0000 Subject: [PATCH 08/12] Add line to caller info --- src/py/reactpy/reactpy/core/hooks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/py/reactpy/reactpy/core/hooks.py b/src/py/reactpy/reactpy/core/hooks.py index 7279b0191..d1a042321 100644 --- a/src/py/reactpy/reactpy/core/hooks.py +++ b/src/py/reactpy/reactpy/core/hooks.py @@ -3,6 +3,7 @@ import asyncio from functools import lru_cache import hashlib +import linecache import sys from collections.abc import Coroutine, Sequence from hashlib import md5 @@ -103,7 +104,10 @@ def get_caller_info(): if patch_path is not None: break # Extract the relevant information: file path and line number and hash it - return f"{caller_frame.f_code.co_filename} {caller_frame.f_lineno} {patch_path}" + filename = caller_frame.f_code.co_filename + lineno = caller_frame.f_lineno + line = linecache.getline(filename, lineno) + return f"{filename} {lineno} {line}, {patch_path}" __DEBUG_CALLER_INFO_TO_STATE_KEY = {} From 1d4c1464cc8b80881e930bf6c165520db45ff24d Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Fri, 17 May 2024 23:58:34 +0000 Subject: [PATCH 09/12] comment update --- src/py/reactpy/reactpy/core/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/reactpy/reactpy/core/hooks.py b/src/py/reactpy/reactpy/core/hooks.py index d1a042321..f0434789f 100644 --- a/src/py/reactpy/reactpy/core/hooks.py +++ b/src/py/reactpy/reactpy/core/hooks.py @@ -103,10 +103,10 @@ def get_caller_info(): patch_path = render_frame.f_locals.get("patch_path_for_state") if patch_path is not None: break - # Extract the relevant information: file path and line number and hash it + # Extract the relevant information: file path, line number, and line and hash it filename = caller_frame.f_code.co_filename lineno = caller_frame.f_lineno - line = linecache.getline(filename, lineno) + line = linecache.getline(filename, lineno) return f"{filename} {lineno} {line}, {patch_path}" From 1803ea460240cdb992c203b5c2df74c3872667b7 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Mon, 1 Jul 2024 01:39:41 +0000 Subject: [PATCH 10/12] Fix for dictionary changed size during iteration --- 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 59c8c89da..eeea4d8ab 100644 --- a/src/py/reactpy/reactpy/core/state_recovery.py +++ b/src/py/reactpy/reactpy/core/state_recovery.py @@ -158,7 +158,7 @@ async def serialize_state_vars( ) return {} result = {} - for chunk in chunked(state_vars.items(), 50): + for chunk in chunked(list(state_vars.items()), 50): for key, value in chunk: result[key] = self._serialize(key, value) await asyncio.sleep(0) # relinquish CPU From 1b73b8816cbfa5cbd08fc7807901348616b0cb63 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:58:11 -0700 Subject: [PATCH 11/12] Fix reconnection location (#3) * recalc url on reconnection * Don't use serverLocation for reconnection * remove unused variable --- src/js/packages/@reactpy/client/src/reactpy-client.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 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 a934308ef..b57ab4dc1 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-client.ts +++ b/src/js/packages/@reactpy/client/src/reactpy-client.ts @@ -163,7 +163,7 @@ enum messageTypes { export class SimpleReactPyClient extends BaseReactPyClient implements ReactPyClient { - private readonly urls: ServerUrls; + private urls: ServerUrls; private socket!: { current?: WebSocket }; private idleDisconnectTimeMillis: number; private lastActivityTime: number; @@ -390,6 +390,14 @@ export class SimpleReactPyClient lastAttempt = lastAttempt || Date.now(); this.shouldReconnect = true; + this.urls = getServerUrls( + { + url: document.location.origin, + route: document.location.pathname, + query: document.location.search, + }, + ); + window.setTimeout(() => { if (!this.didReconnectingCallback && this.reconnectingCallback && maxRetries != connectionAttemptsRemaining) { this.didReconnectingCallback = true; From 0514dc8397a10ee7fc83ade207f3939c5de9c324 Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:09:35 +0000 Subject: [PATCH 12/12] keep track of state if onChange not given --- src/js/packages/@reactpy/client/src/components.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/packages/@reactpy/client/src/components.tsx b/src/js/packages/@reactpy/client/src/components.tsx index 0f1c1722d..d005c5792 100644 --- a/src/js/packages/@reactpy/client/src/components.tsx +++ b/src/js/packages/@reactpy/client/src/components.tsx @@ -99,6 +99,11 @@ function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element { // allow the client to respond (and possibly change the value) givenOnChange(event); }; + } else if (!givenOnChange) { + props.onChange = (event: ChangeEvent) => { + // set the value so rerender doesn't stomp on state + setValue(event.target.value); + } } // Use createElement here to avoid warning about variable numbers of children not