diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b81c68f..5ae17cf8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,9 @@ Don't forget to remove deprecated code on each major release! - Added `reactpy.reactjs.component_from_npm` to import ReactJS components from NPM. - Added `reactpy.h` as a shorthand alias for `reactpy.html`. - Added `reactpy.config.REACTPY_MAX_QUEUE_SIZE` to configure the maximum size of all ReactPy asyncio queues (e.g. receive buffer, send buffer, event buffer) before ReactPy begins waiting until a slot frees up. This can be used to constraint memory usage. +- Events now support `debounce` and `throttle`, configurable per event via `event.debounce = ` and `EventHandler(fn, throttle=)` respectively. + - `debounce` waits until activity stops, then fires once. Default is 200 ms on `input`/`select`/`textarea`, 0 ms elsewhere. + - `throttle` caps the rate how often an event is allowed to execute. No default; opt in per event. ### Changed @@ -60,9 +63,8 @@ Don't forget to remove deprecated code on each major release! - `reactpy.core.vdom._CustomVdomDictConstructor` has been moved to `reactpy.types.CustomVdomConstructor`. - `reactpy.core.vdom._EllipsisRepr` has been moved to `reactpy.types.EllipsisRepr`. - `reactpy.types.VdomDictConstructor` has been renamed to `reactpy.types.VdomConstructor`. -- `REACTPY_ASYNC_RENDERING` can now de-duplicate and cascade renders where necessary. +- `REACTPY_ASYNC_RENDERING` can now de-duplicate renders where necessary. - `REACTPY_ASYNC_RENDERING` is now defaulted to `True` for up to 40x performance improvements in environments with high concurrency. -- Events now support debounce, which can now be configured per event with `event.debounce = `. Note that `input`, `select`, and `textarea` elements default to 200ms debounce. ### Deprecated diff --git a/pyproject.toml b/pyproject.toml index 23d915d8f..f3124c996 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -191,7 +191,10 @@ check = [ 'bun run --cwd "{root}/src/js/packages/@reactpy/client" checkTypes', 'bun run --cwd "{root}/src/js/packages/@reactpy/app" checkTypes', ] -fix = ['bun install --cwd "{root}/src/js"', 'bun run --cwd "{root}/src/js" format'] +fix = [ + 'bun install --cwd "{root}/src/js"', + 'bun run --cwd "{root}/src/js" format', +] test = ['hatch run javascript:build_event_to_object --dev', 'bun test'] build = [ 'hatch run "{root}/src/build_scripts/clean_js_dir.py"', @@ -206,7 +209,9 @@ build = [ build_event_to_object = [ 'hatch run "{root}/src/build_scripts/build_js_event_to_object.py" {args}', ] -build_client = ['hatch run "{root}/src/build_scripts/build_js_client.py" {args}'] +build_client = [ + 'hatch run "{root}/src/build_scripts/build_js_client.py" {args}', +] build_app = ['hatch run "{root}/src/build_scripts/build_js_app.py" {args}'] publish_event_to_object = [ 'hatch run javascript:build_event_to_object', diff --git a/src/build_scripts/install_deps.py b/src/build_scripts/install_deps.py new file mode 100644 index 000000000..8045d0325 --- /dev/null +++ b/src/build_scripts/install_deps.py @@ -0,0 +1,44 @@ +""" +Development/debug script to parse pyproject.toml to find dependecies then install them in the local +environment via `uv pip install -U ` +""" + +import subprocess +import tomllib as toml +from pathlib import Path + +DEPENDENCIES = set() + + +def find_deps(data): + """Recurse through all categories and find any list with `dependencies` in the name, then combine + all dependencies into a single list""" + if isinstance(data, dict): + for key, value in data.items(): + if ( + "dependencies" in key + and isinstance(value, list) + and value + and isinstance(value[0], str) + ): + DEPENDENCIES.update(value) + else: + find_deps(value) + elif isinstance(data, list): + for item in data: + find_deps(item) + + +def install_deps(): + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + with open(pyproject_path, "rb") as f: + pyproject_data = toml.load(f) + find_deps(pyproject_data) + DEPENDENCIES.discard( + "ruff" + ) # ruff only exists in dev dependencies for CI purposes. + subprocess.run(["uv", "pip", "install", "-U", *DEPENDENCIES], check=False) # noqa: S607,S603 + + +if __name__ == "__main__": + install_deps() diff --git a/src/js/packages/@reactpy/client/src/components.tsx b/src/js/packages/@reactpy/client/src/components.tsx index 2ce16d5fa..e51828574 100644 --- a/src/js/packages/@reactpy/client/src/components.tsx +++ b/src/js/packages/@reactpy/client/src/components.tsx @@ -8,6 +8,14 @@ import { type TargetedEvent, } from "preact"; import { useContext, useEffect, useRef, useState } from "preact/hooks"; +import { + HANDLER_DEBOUNCE, + HANDLER_MARKER, + HANDLER_THROTTLE, + getInitialDebounce, + isValidDebounce, + type TaggedEventHandler, +} from "./handler"; import type { ImportSourceBinding, ReactPyComponent, @@ -18,12 +26,25 @@ import type { ReactPyClient } from "./client"; const ClientContext = createContext(null as any); -const DEFAULT_INPUT_DEBOUNCE = 200; - -type ReactPyInputHandler = ((event: TargetedEvent) => void) & { - debounce?: number; - isHandler?: boolean; -}; +// Built-in default debounce (ms) used by user-input elements (````, +// ``