Skip to content

Commit dbbcf20

Browse files
committed
chore: improve events timer
1 parent cd866d2 commit dbbcf20

File tree

7 files changed

+221
-73
lines changed

7 files changed

+221
-73
lines changed

packages/pluggableWidgets/events-web/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- We fixed an issue with burst executon in inactive tabs.
12+
13+
### Changed
14+
15+
- Repeated execution is not executing next action if previous execution is not yet finished.
16+
917
## [1.1.0] - 2025-08-12
1018

1119
### Added

packages/pluggableWidgets/events-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mendix/app-events-web",
33
"widgetName": "Events",
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"description": "Events",
66
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
77
"license": "Apache-2.0",
Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import classnames from "classnames";
2-
import { EditableValue } from "mendix";
3-
import { ReactElement, useRef } from "react";
2+
import { ReactElement } from "react";
43
import { EventsContainerProps } from "../typings/EventsProps";
5-
import { useActionTimer } from "./hooks/timer";
4+
import { useOnLoadTimer } from "./hooks/useOnLoadTimer";
5+
import { useAttributeMonitor } from "./hooks/useAttributeMonitor";
66
import { useParameterValue } from "./hooks/parameterValue";
77
import "./ui/Events.scss";
88

@@ -23,7 +23,6 @@ export default function Events(props: EventsContainerProps): ReactElement {
2323
onEventChangeDelayParameterType,
2424
onEventChangeDelayExpression
2525
} = props;
26-
const prevOnChangeAttributeValue = useRef<EditableValue<any> | undefined>(undefined);
2726

2827
const delayValue = useParameterValue({
2928
parameterType: componentLoadDelayParameterType,
@@ -41,33 +40,21 @@ export default function Events(props: EventsContainerProps): ReactElement {
4140
parameterExpression: onEventChangeDelayExpression
4241
});
4342

44-
useActionTimer({
45-
canExecute: onComponentLoad?.canExecute,
43+
useOnLoadTimer({
44+
canExecute: onComponentLoad ? onComponentLoad.canExecute && !onComponentLoad.isExecuting : false,
4645
execute: onComponentLoad?.execute,
4746
delay: delayValue,
4847
interval: intervalValue,
4948
repeat: componentLoadRepeat,
5049
attribute: undefined
5150
});
52-
useActionTimer({
53-
canExecute: onEventChange?.canExecute,
54-
execute: () => {
55-
if (onEventChangeAttribute?.status === "loading") {
56-
return;
57-
}
58-
if (prevOnChangeAttributeValue?.current?.value === undefined) {
59-
prevOnChangeAttributeValue.current = onEventChangeAttribute;
60-
} else {
61-
if (onEventChangeAttribute?.value !== prevOnChangeAttributeValue.current?.value) {
62-
prevOnChangeAttributeValue.current = onEventChangeAttribute;
63-
onEventChange?.execute();
64-
}
65-
}
66-
},
51+
52+
useAttributeMonitor({
53+
canExecute: onEventChange ? onEventChange.canExecute && !onEventChange.isExecuting : false,
54+
execute: onEventChange?.execute,
6755
delay: onEventChangeDelayValue,
68-
interval: 0,
69-
repeat: false,
7056
attribute: onEventChangeAttribute
7157
});
58+
7259
return <div className={classnames("widget-events", className)}></div>;
7360
}

packages/pluggableWidgets/events-web/src/hooks/timer.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { EditableValue } from "mendix";
2+
import { useEffect, useState } from "react";
3+
import { debounce } from "@mendix/widget-plugin-platform/utils/debounce";
4+
5+
interface UseAttributeMonitorProps {
6+
canExecute: boolean;
7+
execute?: () => void;
8+
delay: number | undefined;
9+
attribute?: EditableValue;
10+
}
11+
12+
class AttributeMonitor {
13+
private currentValue: EditableValue | undefined;
14+
private canExecute = false;
15+
private debouncedCallback: [() => void, () => void] | undefined;
16+
17+
updateCallback(newCb?: () => void, delay?: number): void {
18+
this.debouncedCallback?.[1](); // cancel previous one
19+
if (newCb && delay !== undefined) {
20+
this.debouncedCallback = debounce(newCb, delay);
21+
}
22+
}
23+
24+
updateCanExecute(canExecute: boolean): void {
25+
this.canExecute = canExecute;
26+
}
27+
28+
updateAttribute(newValue?: EditableValue): void {
29+
if (newValue === undefined || newValue.status === "unavailable") {
30+
// value is not present at all or not available, do nothing.
31+
return;
32+
}
33+
34+
if (newValue.status === "loading") {
35+
// value is still loading, wait for it
36+
return;
37+
}
38+
39+
if (this.currentValue === undefined) {
40+
// this is the first load, as the current value is absent
41+
// remember the value and wait for next updates
42+
this.currentValue = newValue;
43+
44+
return;
45+
}
46+
47+
// we got new value, compare it to the current one
48+
// and execute callback if it changed.
49+
if (this.currentValue.value !== newValue.value) {
50+
// todo: execute debounced
51+
// onEventChange?.execute();
52+
this.trigger();
53+
}
54+
55+
// remember the value for the next time
56+
this.currentValue = newValue;
57+
}
58+
59+
trigger(): void {
60+
if (this.canExecute) {
61+
this.debouncedCallback?.[0]();
62+
}
63+
}
64+
65+
stop(): void {
66+
// drop all pending executions
67+
this.debouncedCallback?.[1]();
68+
}
69+
}
70+
71+
export function useAttributeMonitor(props: UseAttributeMonitorProps): void {
72+
const [attributeMonitor] = useState(() => new AttributeMonitor());
73+
74+
// update canExecute props
75+
useEffect(() => {
76+
attributeMonitor.updateCanExecute(props.canExecute);
77+
}, [attributeMonitor, props.canExecute]);
78+
79+
// update callback props
80+
useEffect(() => {
81+
attributeMonitor.updateCallback(props.execute, props.delay);
82+
}, [attributeMonitor, props.execute, props.delay]);
83+
84+
useEffect(() => {
85+
attributeMonitor.updateAttribute(props.attribute);
86+
}, [attributeMonitor, props.attribute]);
87+
88+
// cleanup
89+
useEffect(() => {
90+
return () => {
91+
attributeMonitor.stop();
92+
};
93+
}, [attributeMonitor]);
94+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { EditableValue } from "mendix";
2+
import { useEffect, useState } from "react";
3+
4+
interface UseOnLoadTimerProps {
5+
canExecute: boolean;
6+
execute?: () => void;
7+
delay: number | undefined;
8+
interval: number | undefined;
9+
repeat: boolean;
10+
attribute?: EditableValue;
11+
}
12+
13+
class TimerExecutor {
14+
private intervalHandle: number | undefined;
15+
private isFirstTime: boolean = true;
16+
private isPendingExecution: boolean = false;
17+
private canExecute: boolean = false;
18+
19+
private delay?: number;
20+
private interval?: number;
21+
private repeat?: boolean;
22+
23+
private callback?: () => void;
24+
25+
setCallback(callback: () => void, canExecute: boolean): void {
26+
this.callback = callback;
27+
this.canExecute = canExecute;
28+
29+
this.trigger();
30+
}
31+
32+
setParams(delay: number | undefined, interval: number | undefined, repeat: boolean): void {
33+
this.delay = delay;
34+
this.interval = interval;
35+
this.repeat = repeat;
36+
37+
this.next();
38+
}
39+
40+
get isReady(): boolean {
41+
return this.delay !== undefined && (!this.repeat || this.interval !== undefined);
42+
}
43+
44+
next(): void {
45+
if (!this.isReady) {
46+
return;
47+
}
48+
49+
if (!this.isFirstTime && !this.repeat) {
50+
// we did execute it once, and we don't need to repeat
51+
// so do nothing
52+
return;
53+
}
54+
55+
// schedule a timer
56+
this.intervalHandle = window.setTimeout(
57+
() => {
58+
this.isPendingExecution = true;
59+
this.trigger();
60+
this.isFirstTime = false;
61+
this.next();
62+
},
63+
this.isFirstTime ? this.delay : this.interval
64+
);
65+
}
66+
67+
trigger(): void {
68+
if (this.isPendingExecution && this.canExecute) {
69+
this.isPendingExecution = false;
70+
this.callback?.();
71+
}
72+
}
73+
74+
stop(): void {
75+
window.clearTimeout(this.intervalHandle);
76+
this.intervalHandle = undefined;
77+
this.delay = undefined;
78+
this.interval = undefined;
79+
this.repeat = false;
80+
}
81+
}
82+
83+
export function useOnLoadTimer(props: UseOnLoadTimerProps): void {
84+
const { canExecute, execute, delay, interval, repeat, attribute } = props;
85+
86+
const [timerExecutor] = useState(() => new TimerExecutor());
87+
88+
// update callback props
89+
useEffect(() => {
90+
timerExecutor.setCallback(() => execute?.call(attribute), canExecute);
91+
}, [timerExecutor, execute, attribute, canExecute]);
92+
93+
// update interval props
94+
useEffect(() => {
95+
timerExecutor.setParams(delay, interval, repeat);
96+
return () => {
97+
timerExecutor.stop();
98+
};
99+
}, [timerExecutor, delay, interval, repeat]);
100+
101+
// cleanup
102+
useEffect(() => {
103+
return () => {
104+
timerExecutor.stop();
105+
};
106+
}, [timerExecutor]);
107+
}

packages/pluggableWidgets/events-web/src/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<package xmlns="http://www.mendix.com/package/1.0/">
3-
<clientModule name="Events" version="1.1.0" xmlns="http://www.mendix.com/clientModule/1.0/">
3+
<clientModule name="Events" version="1.2.0" xmlns="http://www.mendix.com/clientModule/1.0/">
44
<widgetFiles>
55
<widgetFile path="Events.xml" />
66
</widgetFiles>

0 commit comments

Comments
 (0)