Skip to content

Commit af1ead5

Browse files
committed
Advanced Polling Features
1 parent 306ad93 commit af1ead5

File tree

8 files changed

+862
-127
lines changed

8 files changed

+862
-127
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# CHANGELOG
22

3+
## 2.29.0
4+
5+
- Added new modifier `limit` and new Hooks for Poll System.
6+
- New Hooks: `poll:started`, `poll:running`, `poll:paused`, `poll:stopped`, `poll:error`
7+
- Handling Count loop and pause, start, stop, resume, error events
8+
39
## 2.28.0
410

5-
- Add new modifiers for input validations, useful to prevent unnecessary HTTP requests:
11+
- Add new modifiers for input validations, useful to prevent uneccessary HTTP requests:
612
- `min_length` and `max_length`: validate length from textual input elements
713
- `min_value` and `max_value`: validate value from numeral input elements
814

@@ -364,7 +370,7 @@ live_component:
364370
- Unexpected Ajax errors are now displayed in a modal to ease debugging! #467.
365371
366372
- Fixed bug where sometimes a live component was broken after hitting "Back:
367-
in your browser - #436.
373+
in your browser" - #436.
368374
369375
## 2.4.0
370376

src/LiveComponent/assets/dist/live_controller.d.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,31 @@ type ComponentHooks = {
8888
'loading.state:started': (element: HTMLElement, request: export_default$1) => MaybePromise;
8989
'loading.state:finished': (element: HTMLElement) => MaybePromise;
9090
'model:set': (model: string, value: any, component: Component) => MaybePromise;
91+
'poll:started': (context: {
92+
actionName: string;
93+
limit: number;
94+
}) => MaybePromise;
95+
'poll:running': (context: {
96+
actionName: string;
97+
count: number;
98+
limit: number;
99+
}) => MaybePromise;
100+
'poll:paused': (context: {
101+
actionName: string;
102+
count: number;
103+
limit: number;
104+
}) => MaybePromise;
105+
'poll:stopped': (context: {
106+
actionName: string;
107+
finalCount: number;
108+
limit: number;
109+
}) => MaybePromise;
110+
'poll:error': (context: {
111+
actionName: string;
112+
finalCount: number;
113+
limit: number;
114+
errorMessage: string;
115+
}) => MaybePromise;
91116
};
92117
type ComponentHookName = keyof ComponentHooks;
93118
type ComponentHookCallback<T extends string = ComponentHookName> = T extends ComponentHookName ? ComponentHooks[T] : (...args: any[]) => MaybePromise;
@@ -120,6 +145,7 @@ declare class Component {
120145
disconnect(): void;
121146
on<T extends string | ComponentHookName = ComponentHookName>(hookName: T, callback: ComponentHookCallback<T>): void;
122147
off<T extends string | ComponentHookName = ComponentHookName>(hookName: T, callback: ComponentHookCallback<T>): void;
148+
triggerPollHook(event: 'poll:started' | 'poll:running' | 'poll:paused' | 'poll:stopped' | 'poll:error', context: any): void;
123149
set(model: string, value: any, reRender?: boolean, debounce?: number | boolean): Promise<export_default$2>;
124150
getData(model: string): any;
125151
action(name: string, args?: any, debounce?: number | boolean): Promise<export_default$2>;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 198 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,6 +1893,14 @@ var Component = class {
18931893
off(hookName, callback) {
18941894
this.hooks.unregister(hookName, callback);
18951895
}
1896+
/**
1897+
* Trigger Polling Hook Events In PollingDirector
1898+
* @param event
1899+
* @param context
1900+
*/
1901+
triggerPollHook(event, context) {
1902+
this.hooks.triggerHook(event, context);
1903+
}
18961904
set(model, value, reRender = false, debounce = false) {
18971905
const promise = this.nextRequestPromise;
18981906
const modelName = normalizeModelName(model);
@@ -2584,52 +2592,195 @@ var PageUnloadingPlugin_default = class {
25842592

25852593
// src/PollingDirector.ts
25862594
var PollingDirector_default = class {
2595+
// Lock system for active polling
25872596
constructor(component) {
2588-
this.isPollingActive = true;
2589-
this.pollingIntervals = [];
2597+
this.pollingIntervals = /* @__PURE__ */ new Map();
2598+
// actionName → intervalId
2599+
this.pollingCounts = /* @__PURE__ */ new Map();
2600+
// actionName → current count
2601+
this.pollingConfigs = /* @__PURE__ */ new Map();
2602+
// All Polling's Map
2603+
this.pollingStates = /* @__PURE__ */ new Map();
2604+
// All Polling's States Map
2605+
this.isPollingRunnig = /* @__PURE__ */ new Map();
25902606
this.component = component;
25912607
}
2592-
addPoll(actionName, duration) {
2593-
this.polls.push({ actionName, duration });
2594-
if (this.isPollingActive) {
2595-
this.initiatePoll(actionName, duration);
2608+
addPoll(actionName, duration, limit = 0) {
2609+
this.pollingConfigs.set(actionName, { duration, limit });
2610+
if (!this.pollingStates.has(actionName)) {
2611+
this.pollingStates.set(actionName, "active");
2612+
}
2613+
if (!this.pollingCounts.has(actionName)) {
2614+
this.pollingCounts.set(actionName, 0);
25962615
}
2616+
this.initiatePoll(actionName, duration);
25972617
}
2618+
/**
2619+
* Start All Pollings In PollingStates Map Entires
2620+
*/
25982621
startAllPolling() {
2599-
if (this.isPollingActive) {
2600-
return;
2622+
for (const [actionName] of this.pollingConfigs.entries()) {
2623+
this.start(actionName);
26012624
}
2602-
this.isPollingActive = true;
2603-
this.polls.forEach(({ actionName, duration }) => {
2604-
this.initiatePoll(actionName, duration);
2605-
});
26062625
}
2626+
/**
2627+
* Stop All Pollings In PollingStates Map Entires
2628+
*/
26072629
stopAllPolling() {
2608-
this.isPollingActive = false;
2609-
this.pollingIntervals.forEach((interval) => {
2610-
clearInterval(interval);
2611-
});
2630+
for (const [actionName] of this.pollingConfigs.entries()) {
2631+
this.stop(actionName);
2632+
}
26122633
}
2613-
clearPolling() {
2614-
this.stopAllPolling();
2615-
this.polls = [];
2616-
this.startAllPolling();
2634+
/**
2635+
* Stop All Pollings and Clear All Pollings Data
2636+
*/
2637+
clearAllPolling(soft = false) {
2638+
for (const intervalId of this.pollingIntervals.values()) {
2639+
clearTimeout(intervalId);
2640+
}
2641+
this.pollingIntervals.clear();
2642+
this.pollingConfigs.clear();
2643+
if (!soft) {
2644+
this.pollingStates.clear();
2645+
this.pollingCounts.clear();
2646+
}
26172647
}
26182648
initiatePoll(actionName, duration) {
2619-
let callback;
2620-
if (actionName === "$render") {
2621-
callback = () => {
2622-
this.component.render();
2623-
};
2624-
} else {
2625-
callback = () => {
2626-
this.component.action(actionName, {}, 0);
2627-
};
2628-
}
2629-
const timer = window.setInterval(() => {
2630-
callback();
2649+
this.isPollingRunnig.set(actionName, false);
2650+
const callback = async () => {
2651+
if (this.isPollingRunnig.get(actionName)) return;
2652+
this.isPollingRunnig.set(actionName, true);
2653+
const limit = this.pollingConfigs.get(actionName)?.limit ?? 0;
2654+
const currentCount = this.pollingCounts.get(actionName) ?? 0;
2655+
if (currentCount === 0) {
2656+
this.component.triggerPollHook("poll:started", { actionName, limit });
2657+
}
2658+
this.pollingCounts.set(actionName, currentCount + 1);
2659+
if (limit > 0 && currentCount >= limit) {
2660+
this.stop(actionName);
2661+
return;
2662+
}
2663+
try {
2664+
if (actionName === "$render") {
2665+
await this.component.render();
2666+
} else {
2667+
const response = await this.component.action(actionName, {}, 0);
2668+
if (response?.response?.status === 500) {
2669+
this.stop(actionName);
2670+
throw new Error(this.decodeErrorMessage(await response.getBody()));
2671+
}
2672+
}
2673+
this.component.triggerPollHook("poll:running", {
2674+
actionName,
2675+
count: currentCount + 1,
2676+
limit
2677+
});
2678+
} catch (error) {
2679+
this.component.triggerPollHook("poll:error", {
2680+
actionName,
2681+
finalCount: currentCount + 1,
2682+
limit,
2683+
errorMessage: error instanceof Error ? error.message : String(error)
2684+
});
2685+
} finally {
2686+
this.isPollingRunnig.set(actionName, false);
2687+
}
2688+
};
2689+
const intervalId = window.setInterval(() => {
2690+
if (this.pollingStates.get(actionName) !== "active") {
2691+
clearInterval(intervalId);
2692+
this.pollingIntervals.delete(actionName);
2693+
return;
2694+
}
2695+
callback().catch((e) => console.error(e));
26312696
}, duration);
2632-
this.pollingIntervals.push(timer);
2697+
this.pollingIntervals.set(actionName, intervalId);
2698+
}
2699+
/**
2700+
* Pause Polling by action Name
2701+
* Pause if polling's status is active only
2702+
*/
2703+
pause(actionName = "$render") {
2704+
if (this.pollingStates.get(actionName) !== "active") return;
2705+
const intervalId = this.pollingIntervals.get(actionName);
2706+
if (intervalId !== void 0) {
2707+
clearInterval(intervalId);
2708+
this.pollingIntervals.delete(actionName);
2709+
}
2710+
this.pollingStates.set(actionName, "paused");
2711+
const count = this.pollingCounts.get(actionName) ?? 0;
2712+
const limit = this.pollingConfigs.get(actionName)?.limit ?? 0;
2713+
this.component.triggerPollHook("poll:paused", { actionName, count, limit });
2714+
}
2715+
/**
2716+
* Resume Polling by action Name
2717+
* Resume if polling's status is paused only
2718+
*/
2719+
resume(actionName = "$render") {
2720+
const config = this.pollingConfigs.get(actionName);
2721+
if (this.pollingStates.get(actionName) !== "paused" || !config) {
2722+
return;
2723+
}
2724+
this.pollingStates.set(actionName, "active");
2725+
this.initiatePoll(actionName, config.duration);
2726+
}
2727+
/**
2728+
* Stop Polling by action Name
2729+
* Stop if polling's status is active or paused
2730+
*/
2731+
stop(actionName = "$render") {
2732+
const state = this.pollingStates.get(actionName);
2733+
if (state !== "active" && state !== "paused") {
2734+
return;
2735+
}
2736+
const intervalId = this.pollingIntervals.get(actionName);
2737+
if (intervalId !== void 0) {
2738+
clearInterval(intervalId);
2739+
this.pollingIntervals.delete(actionName);
2740+
}
2741+
const currentCount = this.pollingCounts.get(actionName) ?? 1;
2742+
const limit = this.pollingConfigs.get(actionName)?.limit ?? 0;
2743+
this.component.triggerPollHook("poll:stopped", {
2744+
actionName,
2745+
finalCount: currentCount,
2746+
limit
2747+
});
2748+
this.pollingCounts.delete(actionName);
2749+
this.pollingStates.set(actionName, "stopped");
2750+
}
2751+
/**
2752+
* Start Polling by action Name
2753+
* Start if polling's status is stopped only
2754+
*/
2755+
start(actionName = "$render") {
2756+
const config = this.pollingConfigs.get(actionName);
2757+
if (!config || this.pollingStates.get(actionName) !== "stopped") return;
2758+
this.clearForAction(actionName);
2759+
this.pollingCounts.set(actionName, 0);
2760+
this.pollingStates.set(actionName, "active");
2761+
this.initiatePoll(actionName, config.duration);
2762+
}
2763+
/**
2764+
* Clear Polling Count and Interval data by action Name
2765+
*/
2766+
clearForAction(actionName = "$render") {
2767+
this.pollingCounts.delete(actionName);
2768+
const intervalId = this.pollingIntervals.get(actionName);
2769+
if (intervalId !== void 0) {
2770+
clearInterval(intervalId);
2771+
this.pollingIntervals.delete(actionName);
2772+
}
2773+
}
2774+
/**
2775+
* Decode Error Message
2776+
*/
2777+
decodeErrorMessage(errorMessage) {
2778+
errorMessage = errorMessage.split("<!--")[1]?.split("-->")[0]?.trim();
2779+
if (errorMessage) {
2780+
const decoded = errorMessage.replace(/&quot;/g, '"').replace(/&#039;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
2781+
return `Poll\xB7error:\xB7${decoded}`;
2782+
}
2783+
return "Poll error: 500 Internal Server Error";
26332784
}
26342785
};
26352786

@@ -2639,6 +2790,7 @@ var PollingPlugin_default = class {
26392790
this.element = component.element;
26402791
this.pollingDirector = new PollingDirector_default(component);
26412792
this.initializePolling();
2793+
component.pollingDirector = this.pollingDirector;
26422794
component.on("connect", () => {
26432795
this.pollingDirector.startAllPolling();
26442796
});
@@ -2649,11 +2801,11 @@ var PollingPlugin_default = class {
26492801
this.initializePolling();
26502802
});
26512803
}
2652-
addPoll(actionName, duration) {
2653-
this.pollingDirector.addPoll(actionName, duration);
2804+
addPoll(actionName, duration, limit) {
2805+
this.pollingDirector.addPoll(actionName, duration, limit);
26542806
}
26552807
clearPolling() {
2656-
this.pollingDirector.clearPolling();
2808+
this.pollingDirector.clearAllPolling(true);
26572809
}
26582810
initializePolling() {
26592811
this.clearPolling();
@@ -2664,18 +2816,26 @@ var PollingPlugin_default = class {
26642816
const directives = parseDirectives(rawPollConfig || "$render");
26652817
directives.forEach((directive) => {
26662818
let duration = 2e3;
2819+
let limit = 0;
26672820
directive.modifiers.forEach((modifier) => {
26682821
switch (modifier.name) {
26692822
case "delay":
26702823
if (modifier.value) {
2671-
duration = Number.parseInt(modifier.value);
2824+
const parsed = Number.parseInt(modifier.value);
2825+
duration = Number.isNaN(parsed) || parsed <= 0 ? 2e3 : parsed;
2826+
}
2827+
break;
2828+
case "limit":
2829+
if (modifier.value) {
2830+
const parsed = Number.parseInt(modifier.value);
2831+
limit = Number.isNaN(parsed) || parsed <= 0 ? 1 : parsed;
26722832
}
26732833
break;
26742834
default:
26752835
console.warn(`Unknown modifier "${modifier.name}" in data-poll "${rawPollConfig}".`);
26762836
}
26772837
});
2678-
this.addPoll(directive.action, duration);
2838+
this.addPoll(directive.action, duration, limit);
26792839
});
26802840
}
26812841
};

src/LiveComponent/assets/src/Component/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ export type ComponentHooks = {
2525
'loading.state:started': (element: HTMLElement, request: BackendRequest) => MaybePromise;
2626
'loading.state:finished': (element: HTMLElement) => MaybePromise;
2727
'model:set': (model: string, value: any, component: Component) => MaybePromise;
28+
'poll:started': (context: { actionName: string; limit: number }) => MaybePromise;
29+
'poll:running': (context: { actionName: string; count: number; limit: number }) => MaybePromise;
30+
'poll:paused': (context: { actionName: string; count: number; limit: number }) => MaybePromise;
31+
'poll:stopped': (context: { actionName: string; finalCount: number; limit: number }) => MaybePromise;
32+
'poll:error': (context: {
33+
actionName: string;
34+
finalCount: number;
35+
limit: number;
36+
errorMessage: string;
37+
}) => MaybePromise;
2838
};
2939

3040
export type ComponentHookName = keyof ComponentHooks;
@@ -149,6 +159,19 @@ export default class Component {
149159
this.hooks.unregister(hookName, callback);
150160
}
151161

162+
/**
163+
* Trigger Polling Hook Events In PollingDirector
164+
* @param event
165+
* @param context
166+
*/
167+
168+
public triggerPollHook(
169+
event: 'poll:started' | 'poll:running' | 'poll:paused' | 'poll:stopped' | 'poll:error',
170+
context: any
171+
) {
172+
this.hooks.triggerHook(event, context);
173+
}
174+
152175
set(model: string, value: any, reRender = false, debounce: number | boolean = false): Promise<BackendResponse> {
153176
const promise = this.nextRequestPromise;
154177
const modelName = normalizeModelName(model);

0 commit comments

Comments
 (0)