Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
96 changes: 58 additions & 38 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,53 @@ export const ReflexEvent = (
return { name, payload, handler, event_actions };
};

/**
* Apply event actions before invoking a target function.
* @param {Function} target The function to invoke after applying event actions.
* @param {Object.<string, (number|boolean)>} event_actions The actions to apply.
* @param {Array<any>|any} args The event args.
* @param {string|null} action_key A stable key for debounce/throttle tracking.
* @param {Function|null} temporal_handler Returns whether temporal actions may run.
* @returns The target result, if it runs immediately.
*/
export const applyEventActions = (
target,
event_actions = {},
args = [],
action_key = null,
temporal_handler = null,
) => {
if (!(args instanceof Array)) {
args = [args];
}

const _e = args.find((o) => o?.preventDefault !== undefined);

if (event_actions?.preventDefault && _e?.preventDefault) {
_e.preventDefault();
}
if (event_actions?.stopPropagation && _e?.stopPropagation) {
_e.stopPropagation();
}
if (event_actions?.temporal && temporal_handler && !temporal_handler()) {
return;
}

const invokeTarget = () => target(...args);
const resolved_action_key = action_key ?? target.toString();

if (event_actions?.throttle) {
if (!throttle(resolved_action_key, event_actions.throttle)) {
return;
}
}
if (event_actions?.debounce) {
debounce(resolved_action_key, invokeTarget, event_actions.debounce);
return;
}
return invokeTarget();
};

/**
* Package client-side storage values as payload to send to the
* backend with the hydrate event
Expand Down Expand Up @@ -898,51 +945,24 @@ export const useEventLoop = (
// Function to add new events to the event queue.
const addEvents = useCallback((events, args, event_actions) => {
const _events = events.filter((e) => e !== undefined && e !== null);
if (!event_actions?.temporal) {
// Reconnect socket if needed for non-temporal events.
ensureSocketConnected();
}

if (!(args instanceof Array)) {
args = [args];
}

event_actions = _events.reduce(
(acc, e) => ({ ...acc, ...e.event_actions }),
event_actions ?? {},
);

const _e = args.filter((o) => o?.preventDefault !== undefined)[0];

if (event_actions?.preventDefault && _e?.preventDefault) {
_e.preventDefault();
}
if (event_actions?.stopPropagation && _e?.stopPropagation) {
_e.stopPropagation();
}
const combined_name = _events.map((e) => e.name).join("+++");
if (event_actions?.temporal) {
if (!socket.current || !socket.current.connected) {
return; // don't queue when the backend is not connected
}
}
if (event_actions?.throttle) {
// If throttle returns false, the events are not added to the queue.
if (!throttle(combined_name, event_actions.throttle)) {
return;
}
}
if (event_actions?.debounce) {
// If debounce is used, queue the events after some delay
debounce(
combined_name,
() =>
queueEvents(_events, socket, false, navigate, () => params.current),
event_actions.debounce,
);
} else {
queueEvents(_events, socket, false, navigate, () => params.current);
if (!event_actions?.temporal) {
// Reconnect socket if needed for non-temporal events.
ensureSocketConnected();
}

return applyEventActions(
() => queueEvents(_events, socket, false, navigate, () => params.current),
event_actions,
args,
_events.map((e) => e.name).join("+++"),
() => !!socket.current?.connected,
);
}, []);

const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode
Expand Down
10 changes: 5 additions & 5 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1934,11 +1934,6 @@ async def upload_file(request: Request):
Returns:
StreamingResponse yielding newline-delimited JSON of StateUpdate
emitted by the upload handler.

Raises:
UploadValueError: if there are no args with supported annotation.
UploadTypeError: if a background task is used as the handler.
HTTPException: when the request does not include token / handler headers.
"""
from reflex.utils.exceptions import UploadTypeError, UploadValueError

Expand All @@ -1963,6 +1958,11 @@ async def _create_upload_event() -> Event:

Returns:
The upload event backed by the original temp files.

Raises:
UploadValueError: If there are no uploaded files or supported annotations.
UploadTypeError: If a background task is used as the handler.
HTTPException: If the request is missing token or handler headers.
"""
files = form_data.getlist("files")
if not files:
Expand Down
7 changes: 6 additions & 1 deletion reflex/constants/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class CompileVars(SimpleNamespace):
IS_HYDRATED = "is_hydrated"
# The name of the function to add events to the queue.
ADD_EVENTS = "addEvents"
# The name of the function to apply event actions before invoking a target.
APPLY_EVENT_ACTIONS = "applyEventActions"
# The name of the var storing any connection error.
CONNECT_ERROR = "connectErrors"
# The name of the function for converting a dict to an event.
Expand Down Expand Up @@ -128,7 +130,10 @@ class Imports(SimpleNamespace):
EVENTS = {
"react": [ImportVar(tag="useContext")],
f"$/{Dirs.CONTEXTS_PATH}": [ImportVar(tag="EventLoopContext")],
f"$/{Dirs.STATE_PATH}": [ImportVar(tag=CompileVars.TO_EVENT)],
f"$/{Dirs.STATE_PATH}": [
ImportVar(tag=CompileVars.TO_EVENT),
ImportVar(tag=CompileVars.APPLY_EVENT_ACTIONS),
],
}


Expand Down
79 changes: 66 additions & 13 deletions reflex/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import inspect
import sys
import types
import warnings
from base64 import b64encode
from collections.abc import Callable, Mapping, Sequence
from functools import lru_cache, partial
Expand Down Expand Up @@ -449,8 +450,8 @@ def __call__(self, *args, **kwargs) -> EventSpec:
class EventChain(EventActionsMixin):
"""Container for a chain of events that will be executed in order."""

events: Sequence["EventSpec | EventVar | EventCallback"] = dataclasses.field(
default_factory=list
events: Sequence["EventSpec | EventVar | FunctionVar | EventCallback"] = (
dataclasses.field(default_factory=list)
)

args_spec: Callable | Sequence[Callable] | None = dataclasses.field(default=None)
Expand Down Expand Up @@ -481,9 +482,18 @@ def create(
"""
# If it's an event chain var, return it.
if isinstance(value, Var):
if isinstance(value, EventChainVar):
# Only pass through literal/prebuilt chains. Other EventChainVar values may be
# FunctionVars cast with `.to(EventChain)` and still need wrapping so
# event_chain_kwargs can compose onto the resulting chain.
if isinstance(value, LiteralEventChainVar):
Comment thread
masenf marked this conversation as resolved.
if event_chain_kwargs:
warnings.warn(
f"event_chain_kwargs {event_chain_kwargs!r} are ignored for "
"EventChainVar values.",
stacklevel=2,
)
return value
Comment thread
masenf marked this conversation as resolved.
if isinstance(value, EventVar):
if isinstance(value, (EventVar, FunctionVar)):
value = [value]
elif safe_issubclass(value._var_type, (EventChain, EventSpec)):
return cls.create(
Expand All @@ -505,23 +515,26 @@ def create(

# If the input is a list of event handlers, create an event chain.
if isinstance(value, list):
events: list[EventSpec | EventVar] = []
events: list[EventSpec | EventVar | FunctionVar] = []
for v in value:
if isinstance(v, (EventHandler, EventSpec)):
# Call the event handler to get the event.
events.append(call_event_handler(v, args_spec, key=key))
elif isinstance(v, (EventVar, FunctionVar)):
events.append(v)
elif isinstance(v, Callable):
# Call the lambda to get the event chain.
result = call_event_fn(v, args_spec, key=key)
if isinstance(result, Var):
if isinstance(result, (EventVar, FunctionVar)):
events.append(result)
continue
msg = (
f"Invalid event chain: {v}. Cannot use a Var-returning "
"lambda inside an EventChain list."
)
raise ValueError(msg)
events.extend(result)
elif isinstance(v, EventVar):
events.append(v)
else:
msg = f"Invalid event: {v}"
raise ValueError(msg)
Expand Down Expand Up @@ -2075,9 +2088,11 @@ def create(
else value.args_spec
)
sig = inspect.signature(arg_spec) # pyright: ignore [reportArgumentType]
arg_vars = ()
if sig.parameters:
arg_def = tuple(f"_{p}" for p in sig.parameters)
arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
arg_vars = tuple(Var(_js_expr=arg) for arg in arg_def)
arg_def_expr = LiteralVar.create(list(arg_vars))
else:
# add a default argument for addEvents if none were specified in value.args_spec
# used to trigger the preventDefault() on the event.
Expand All @@ -2098,17 +2113,55 @@ def create(
if invocation is not None and not isinstance(invocation, FunctionVar):
msg = f"EventChain invocation must be a FunctionVar, got {invocation!s} of type {invocation._var_type!s}."
raise ValueError(msg)
assert invocation is not None

call_args = arg_vars if sig.parameters else (Var(_js_expr="...args"),)
statements = [
(
event.call(*call_args)
if isinstance(event, FunctionVar)
else invocation.call(
LiteralVar.create([LiteralVar.create(event)]),
arg_def_expr,
{},
)
)
for event in value.events
]

if not statements:
statements.append(invocation.call(LiteralVar.create([]), arg_def_expr, {}))

statement_var_data = [statement._get_all_var_data() for statement in statements]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this might be the line that is causing the CodSpeed regression.

usually if you just combine the vars, the parser will pick up any nested VarData automatically:

            statement_block = Var(
                _js_expr=f"{{{''.join(f'{statement};' for statement in statements)}}}",
            )

i would expect the above to be equivalent to how it was written without the explicit VarData merge

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, it is fixed now.

if len(statements) == 1 and not value.event_actions:
return_expr = statements[0]
else:
statement_block = Var(
_js_expr=f"{{{''.join(f'{statement!s};' for statement in statements)}}}",
_var_data=VarData.merge(*statement_var_data),
)
if value.event_actions:
apply_event_actions = FunctionStringVar.create(
CompileVars.APPLY_EVENT_ACTIONS,
_var_data=VarData(
imports=Imports.EVENTS,
hooks={Hooks.EVENTS: None},
),
)
return_expr = apply_event_actions.call(
ArgsFunctionOperation.create((), statement_block),
value.event_actions,
*call_args,
)
else:
return_expr = statement_block

return cls(
_js_expr="",
_var_type=EventChain,
_var_data=_var_data,
_args=FunctionArgs(arg_def),
_return_expr=invocation.call(
LiteralVar.create([LiteralVar.create(event) for event in value.events]),
arg_def_expr,
value.event_actions,
),
_return_expr=return_expr,
_var_value=value,
)

Expand Down
Loading
Loading