From 5588eb03fa9474e06a24c7d1c816489825844151 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 21 May 2026 17:25:29 +0200 Subject: [PATCH 01/11] chore(pusher-web): initial implementation # Conflicts: # pnpm-lock.yaml --- .../pusher-web/.prettierrc.js | 5 + .../pluggableWidgets/pusher-web/CHANGELOG.md | 11 + .../pluggableWidgets/pusher-web/README.md | 1 + .../pusher-web/eslint.config.mjs | 3 + .../pluggableWidgets/pusher-web/package.json | 62 +++ .../pusher-web/src/Pusher.editorConfig.ts | 31 ++ .../pusher-web/src/Pusher.editorPreview.tsx | 9 + .../pusher-web/src/Pusher.tsx | 50 +++ .../pusher-web/src/Pusher.xml | 24 + .../pusher-web/src/__tests__/Pusher.spec.tsx | 7 + .../pusher-web/src/hooks/usePusherConfig.ts | 55 +++ .../pusher-web/src/hooks/usePusherListener.ts | 51 +++ .../pusher-web/src/package.xml | 11 + .../pusher-web/src/ui/Pusher.scss | 0 .../pusher-web/src/ui/PusherPreview.css | 6 + .../pusher-web/src/utils/PusherListener.ts | 127 ++++++ .../pusher-web/src/utils/useMxObjectInfo.ts | 32 ++ .../pluggableWidgets/pusher-web/tsconfig.json | 30 ++ .../pusher-web/typings/PusherProps.d.ts | 33 ++ pnpm-lock.yaml | 420 +++++++----------- 20 files changed, 719 insertions(+), 249 deletions(-) create mode 100644 packages/pluggableWidgets/pusher-web/.prettierrc.js create mode 100644 packages/pluggableWidgets/pusher-web/CHANGELOG.md create mode 100644 packages/pluggableWidgets/pusher-web/README.md create mode 100644 packages/pluggableWidgets/pusher-web/eslint.config.mjs create mode 100644 packages/pluggableWidgets/pusher-web/package.json create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.tsx create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.xml create mode 100644 packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/package.xml create mode 100644 packages/pluggableWidgets/pusher-web/src/ui/Pusher.scss create mode 100644 packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css create mode 100644 packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts create mode 100644 packages/pluggableWidgets/pusher-web/tsconfig.json create mode 100644 packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts diff --git a/packages/pluggableWidgets/pusher-web/.prettierrc.js b/packages/pluggableWidgets/pusher-web/.prettierrc.js new file mode 100644 index 0000000000..13dc01f67f --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/.prettierrc.js @@ -0,0 +1,5 @@ +const base = require("@mendix/prettier-config-web-widgets"); + +module.exports = { + ...base +}; diff --git a/packages/pluggableWidgets/pusher-web/CHANGELOG.md b/packages/pluggableWidgets/pusher-web/CHANGELOG.md new file mode 100644 index 0000000000..d3fa2771ff --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this widget will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial widget scaffolding diff --git a/packages/pluggableWidgets/pusher-web/README.md b/packages/pluggableWidgets/pusher-web/README.md new file mode 100644 index 0000000000..cdcf2addda --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/README.md @@ -0,0 +1 @@ +# Pusher Widget diff --git a/packages/pluggableWidgets/pusher-web/eslint.config.mjs b/packages/pluggableWidgets/pusher-web/eslint.config.mjs new file mode 100644 index 0000000000..ed68ae9e78 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs"; + +export default config; diff --git a/packages/pluggableWidgets/pusher-web/package.json b/packages/pluggableWidgets/pusher-web/package.json new file mode 100644 index 0000000000..21b64c822e --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/package.json @@ -0,0 +1,62 @@ +{ + "name": "@mendix/pusher-web", + "widgetName": "Pusher", + "version": "2.0.0", + "description": "Pusher.com integration widget for real-time communication", + "copyright": "© Mendix Technology BV 2026. All rights reserved.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/mendix/web-widgets.git" + }, + "config": { + "developmentPort": 3000, + "mendixHost": "http://localhost:8080" + }, + "mxpackage": { + "name": "Pusher", + "type": "widget", + "mpkName": "com.mendix.widget.web.Pusher.mpk" + }, + "packagePath": "com.mendix.widget.web", + "marketplace": { + "minimumMXVersion": "11.11.0", + "appName": "Pusher", + "reactReady": true + }, + "testProject": { + "githubUrl": "https://github.com/mendix/testProjects", + "branchName": "pusher-web" + }, + "scripts": { + "build": "pluggable-widgets-tools build:web", + "create-gh-release": "rui-create-gh-release", + "create-translation": "rui-create-translation", + "dev": "pluggable-widgets-tools start:web", + "e2e": "echo \"Skipping this e2e test\"", + "e2edev": "run-e2e dev --with-preps", + "format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .", + "lint": "eslint src/ package.json", + "release": "pluggable-widgets-tools release:web", + "start": "pluggable-widgets-tools start:server", + "test": "pluggable-widgets-tools test:unit:web", + "update-changelog": "rui-update-changelog-widget", + "verify": "rui-verify-package-format" + }, + "dependencies": { + "classnames": "^2.5.1", + "pusher-js": "^8.5.0" + }, + "devDependencies": { + "@mendix/automation-utils": "workspace:*", + "@mendix/eslint-config-web-widgets": "workspace:*", + "@mendix/pluggable-widgets-tools": "*", + "@mendix/prettier-config-web-widgets": "workspace:*", + "@mendix/run-e2e": "workspace:^*", + "@mendix/widget-plugin-component-kit": "workspace:*", + "@mendix/widget-plugin-hooks": "workspace:*", + "@mendix/widget-plugin-platform": "workspace:*", + "@mendix/widget-plugin-test-utils": "workspace:*", + "cross-env": "^7.0.3" + } +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts new file mode 100644 index 0000000000..1a7e31068b --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts @@ -0,0 +1,31 @@ +import { Properties } from "@mendix/pluggable-widgets-tools"; +import { + container, + rowLayout, + structurePreviewPalette, + StructurePreviewProps, + text +} from "@mendix/widget-plugin-platform/preview/structure-preview-api"; +import { PusherPreviewProps } from "../typings/PusherProps"; + +export function getProperties(_values: PusherPreviewProps, defaultProperties: Properties): Properties { + return defaultProperties; +} + +export function getPreview(values: PusherPreviewProps, isDarkMode: boolean): StructurePreviewProps { + const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"]; + + return rowLayout({ columnSize: "grow", borders: true, backgroundColor: palette.background.containerFill })( + container()(), + rowLayout({ grow: 2, padding: 8 })(text({ fontColor: palette.text.primary, grow: 10 })(getCaption(values))), + container()() + ); +} + +export function getCustomCaption(values: PusherPreviewProps): string { + return getCaption(values); +} + +export function getCaption(values: PusherPreviewProps): string { + return `Pusher widget [${values.notifyChannelName}]`; +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx new file mode 100644 index 0000000000..9e83c4b766 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx @@ -0,0 +1,9 @@ +import classNames from "classnames"; +import { ReactElement } from "react"; +import { PusherPreviewProps } from "typings/PusherProps"; +import { getCaption } from "./Pusher.editorConfig"; +import "./ui/PusherPreview.css"; + +export function preview(props: PusherPreviewProps): ReactElement { + return
{getCaption(props)}
; +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx new file mode 100644 index 0000000000..3c617a0c44 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -0,0 +1,50 @@ +import classnames from "classnames"; +import { ReactElement, useCallback, useMemo } from "react"; +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import { PusherContainerProps } from "../typings/PusherProps"; +import { usePusherListener } from "./hooks/usePusherListener"; +import "./ui/Pusher.scss"; +import { useMxObjectInfo } from "./utils/useMxObjectInfo"; + +export default function Pusher(props: PusherContainerProps): ReactElement { + const { class: className, objectSource, notifyChannelName, notifyAction } = props; + + // Extract object GUID and entity name from data source + const mxObjectInfo = useMxObjectInfo(objectSource as any); // TODO: fix typings when PWT updated. + + // Event callback - triggered when Pusher event is received + const handleEvent = useCallback( + (data: unknown) => { + console.debug("[Pusher] Event received:", data); + + // Execute configured action + executeAction(notifyAction); + }, + [notifyAction] + ); + + // Error callback + const handleError = useCallback((error: Error) => { + console.error("[Pusher] Subscription error:", error.message); + }, []); + + // Setup stable subscription config + const subscription = useMemo(() => { + if (!mxObjectInfo) { + return undefined; + } + + return { + entityName: mxObjectInfo.entityName, + guid: mxObjectInfo.guid, + eventName: notifyChannelName, + onEvent: handleEvent, + onError: handleError + }; + }, [mxObjectInfo, handleEvent, handleError, notifyChannelName]); + + // Initialize Pusher listener + usePusherListener(subscription); + + return
; +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.xml b/packages/pluggableWidgets/pusher-web/src/Pusher.xml new file mode 100644 index 0000000000..ba46794ffd --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.xml @@ -0,0 +1,24 @@ + + + Pusher + Listen to Notify server action and perform client side action + https://docs.mendix.com/appstore/widgets/pusher + + + + Object to listen + + + + + Notify event name + The name should match the with the 'Notify' parameter `EventName` + + + + Action + + + + + diff --git a/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx b/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx new file mode 100644 index 0000000000..aafb61a263 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx @@ -0,0 +1,7 @@ +describe("Pusher", () => { + // TODO: Add comprehensive unit tests for: + // - PusherListener class (connection, subscription, cleanup) + // - usePusherConfig hook (fetching config) + // - usePusherListener hook (React lifecycle integration) + // - Event handling and action execution +}); diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts new file mode 100644 index 0000000000..b20b53c84a --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { PusherConfig } from "../utils/PusherListener"; + +interface KeyData { + key: string; + cluster: string; +} + +/** + * Fetch Pusher configuration from backend + * Returns null while loading or on error + */ +export function usePusherConfig(): PusherConfig | null { + const [config, setConfig] = useState(null); + + useEffect(() => { + const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; + const endpoint = `${baseUrl}rest/pusher/key`; + const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; + + const authEndpoint = `${baseUrl}rest/pusher/auth`; + + fetch(endpoint, { + method: "GET", + credentials: "same-origin", + headers: { + "X-Csrf-Token": csrfToken + } + }) + .then(response => { + if (response.status !== 200) { + throw new Error(`Failed to fetch Pusher key: HTTP ${response.status}`); + } + return response.text(); + }) + .then(data => { + const keyData = JSON.parse(data) as KeyData; + if (!keyData.key || !keyData.cluster) { + throw new Error("Invalid Pusher key data: missing key or cluster"); + } + setConfig({ + key: keyData.key, + cluster: keyData.cluster, + authEndpoint, + csrfToken + }); + }) + .catch(error => { + console.error("[usePusherConfig] Failed to fetch Pusher configuration:", error); + setConfig(null); + }); + }, []); + + return config; +} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts new file mode 100644 index 0000000000..193011f20c --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef } from "react"; +import { usePusherConfig } from "./usePusherConfig"; +import { PusherListener, SubscriptionConfig } from "../utils/PusherListener"; + +/** + * React hook to manage Pusher listener lifecycle + * Automatically handles initialization, subscription changes, and cleanup + */ +export function usePusherListener(subscription?: SubscriptionConfig): void { + const listenerRef = useRef(null); + + // Fetch Pusher config from backend + const pusherConfig = usePusherConfig(); + + const enabled = !!pusherConfig && !!subscription; + + // Initialize PusherListener once when config is available + useEffect(() => { + if (!enabled) { + return; + } + + const listener = new PusherListener(pusherConfig); + listenerRef.current = listener; + + listener.initialize().catch(error => { + console.error("[usePusherListener] Failed to initialize:", error); + }); + + // Cleanup on unmount or when config changes + return () => { + listener.destroy(); + listenerRef.current = null; + }; + }, [pusherConfig, enabled]); + + // Subscribe/unsubscribe based on subscription config changes + useEffect(() => { + const listener = listenerRef.current; + if (!listener || !subscription) { + return; + } + + listener.subscribe(subscription); + + // Unsubscribe on cleanup or when subscription changes + return () => { + listener.unsubscribe(); + }; + }, [subscription]); +} diff --git a/packages/pluggableWidgets/pusher-web/src/package.xml b/packages/pluggableWidgets/pusher-web/src/package.xml new file mode 100644 index 0000000000..414872761b --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/package.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/pluggableWidgets/pusher-web/src/ui/Pusher.scss b/packages/pluggableWidgets/pusher-web/src/ui/Pusher.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css b/packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css new file mode 100644 index 0000000000..c9a0020357 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css @@ -0,0 +1,6 @@ +.widget-pusher-preview { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; +} diff --git a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts new file mode 100644 index 0000000000..0da56ed040 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts @@ -0,0 +1,127 @@ +import Pusher, { Channel } from "pusher-js"; + +export interface PusherConfig { + key: string; + cluster: string; + authEndpoint: string; + csrfToken: string; +} + +export interface SubscriptionConfig { + entityName: string; + guid: string; + eventName: string; + onEvent: (data: unknown) => void; + onError?: (error: Error) => void; +} + +export class PusherListener { + private pusher: Pusher | null = null; + private currentChannel: Channel | null = null; + private currentChannelName: string | null = null; + private currentEventName: string | null = null; + + constructor(private config: PusherConfig) {} + + /** + * Initialize Pusher connection + * Should be called once on widget mount + */ + async initialize(): Promise { + if (this.pusher) { + return; // Already initialized + } + + this.pusher = new Pusher(this.config.key, { + cluster: this.config.cluster, + authEndpoint: this.config.authEndpoint, + auth: { + headers: { + "X-Csrf-Token": this.config.csrfToken + } + } + }); + + // Setup connection event handlers + this.pusher.connection.bind("error", this.handleConnectionError); + this.pusher.connection.bind("state_change", this.handleStateChange); + } + + /** + * Subscribe to channel for specific object and event + * Automatically unsubscribes from previous channel if different + */ + subscribe(config: SubscriptionConfig): void { + if (!this.pusher) { + throw new Error("PusherListener not initialized. Call initialize() first."); + } + + const channelName = this.buildChannelName(config.entityName, config.guid); + + // If already subscribed to same channel and event, do nothing + if (channelName === this.currentChannelName && config.eventName === this.currentEventName) { + return; + } + + // Unsubscribe from previous channel if exists + this.unsubscribe(); + + // Subscribe to new channel + this.currentChannelName = channelName; + this.currentEventName = config.eventName; + this.currentChannel = this.pusher.subscribe(channelName); + + // Bind event handler + this.currentChannel.bind(config.eventName, config.onEvent); + + // Bind error handler + this.currentChannel.bind("pusher:subscription_error", (error: unknown) => { + console.error(error); + const errorMsg = + error === 515 + ? "Authentication failed. Please verify Pusher configuration constants." + : `Subscription error: ${String(error)}`; + config.onError?.(new Error(errorMsg)); + }); + } + + /** + * Unsubscribe from current channel + */ + unsubscribe(): void { + if (this.currentChannel && this.currentChannelName) { + // Unbind event handler before unsubscribing + if (this.currentEventName) { + this.currentChannel.unbind(this.currentEventName); + } + this.pusher?.unsubscribe(this.currentChannelName); + this.currentChannel = null; + this.currentChannelName = null; + this.currentEventName = null; + } + } + + /** + * Disconnect and cleanup + * Should be called on widget unmount + */ + destroy(): void { + this.unsubscribe(); + if (this.pusher) { + this.pusher.disconnect(); + this.pusher = null; + } + } + + private buildChannelName(entityName: string, guid: string): string { + return `private-${entityName}.${guid}`; + } + + private handleConnectionError = (error: unknown): void => { + console.error("[PusherListener] Connection error:", error); + }; + + private handleStateChange = (states: { previous: string; current: string }): void => { + console.debug(`[PusherListener] State changed: ${states.previous} → ${states.current}`); + }; +} diff --git a/packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts b/packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts new file mode 100644 index 0000000000..9ebc31771e --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts @@ -0,0 +1,32 @@ +import { DynamicValue, ObjectItem } from "mendix"; +import { useMemo } from "react"; + +interface MxObjectInfo { + guid: string; + entityName: string; +} + +export function useMxObjectInfo(objectSource: DynamicValue): MxObjectInfo | undefined { + const object = (objectSource as any)?.value as ObjectItem | undefined; + + const guid = object?.id; + const entityName = object ? extractEntityName(object) : undefined; + return useMemo(() => { + if (!guid || !entityName) { + return undefined; + } + + return { + guid, + entityName + }; + }, [guid, entityName]); +} + +function extractEntityName(object: ObjectItem): string { + const mxObj = (object as any)[Object.getOwnPropertySymbols(object)[0]]; + if (!mxObj) { + throw new Error("Unable to extract entity name. mxObject was not found."); + } + return mxObj.getEntity(); +} diff --git a/packages/pluggableWidgets/pusher-web/tsconfig.json b/packages/pluggableWidgets/pusher-web/tsconfig.json new file mode 100644 index 0000000000..7aa60df0c9 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/tsconfig.json @@ -0,0 +1,30 @@ +{ + "include": ["./src", "./typings"], + "compilerOptions": { + "baseUrl": "./", + "noEmitOnError": true, + "sourceMap": true, + "module": "esnext", + "target": "es6", + "lib": ["esnext", "dom"], + "types": ["jest", "node"], + "moduleResolution": "node", + "declaration": false, + "noLib": false, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "strictFunctionTypes": false, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "useUnknownInCatchVariables": false, + "exactOptionalPropertyTypes": false, + "paths": { + "react-hot-loader/root": ["./hot-typescript.ts"] + } + } +} diff --git a/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts new file mode 100644 index 0000000000..1db883cffb --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts @@ -0,0 +1,33 @@ +/** + * This file was generated from Pusher.xml + * WARNING: All changes made to this file will be overwritten + * @author Mendix Widgets Framework Team + */ +import { CSSProperties } from "react"; +import { ActionValue, ListValue } from "mendix"; + +export interface PusherContainerProps { + name: string; + class: string; + style?: CSSProperties; + tabIndex?: number; + objectSource: ListValue; + notifyChannelName: string; + notifyAction?: ActionValue; +} + +export interface PusherPreviewProps { + /** + * @deprecated Deprecated since version 9.18.0. Please use class property instead. + */ + className: string; + class: string; + style: string; + styleObject?: CSSProperties; + readOnly: boolean; + renderMode: "design" | "xray" | "structure"; + translate: (text: string) => string; + objectSource: {} | { caption: string } | { type: string } | null; + notifyChannelName: string; + notifyAction: {} | null; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c5811416b..a47ec5dead 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2114,6 +2114,46 @@ importers: specifier: ^7.0.3 version: 7.0.3 + packages/pluggableWidgets/pusher-web: + dependencies: + classnames: + specifier: ^2.5.1 + version: 2.5.1 + pusher-js: + specifier: ^8.5.0 + version: 8.5.0 + devDependencies: + '@mendix/automation-utils': + specifier: workspace:* + version: link:../../../automation/utils + '@mendix/eslint-config-web-widgets': + specifier: workspace:* + version: link:../../shared/eslint-config-web-widgets + '@mendix/pluggable-widgets-tools': + specifier: 11.8.0 + version: 11.8.0(patch_hash=93aef2ae0fe74bac64b1ea6c76a0ac387d0acbbc48ff8f8832f081f0f1747148)(@jest/transform@29.7.0)(@jest/types@30.2.0)(@swc/core@1.13.5)(@types/babel__core@7.20.5)(@types/node@24.12.4)(eslint@9.39.3(jiti@2.6.1))(jest-util@30.2.0)(prettier@3.8.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.82.0(@babel/core@7.29.0)(@types/react@19.2.2)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + '@mendix/prettier-config-web-widgets': + specifier: workspace:* + version: link:../../shared/prettier-config-web-widgets + '@mendix/run-e2e': + specifier: workspace:^* + version: link:../../../automation/run-e2e + '@mendix/widget-plugin-component-kit': + specifier: workspace:* + version: link:../../shared/widget-plugin-component-kit + '@mendix/widget-plugin-hooks': + specifier: workspace:* + version: link:../../shared/widget-plugin-hooks + '@mendix/widget-plugin-platform': + specifier: workspace:* + version: link:../../shared/widget-plugin-platform + '@mendix/widget-plugin-test-utils': + specifier: workspace:* + version: link:../../shared/widget-plugin-test-utils + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + packages/pluggableWidgets/range-slider-web: dependencies: '@mendix/widget-plugin-component-kit': @@ -3144,10 +3184,6 @@ packages: peerDependencies: playwright-core: '>= 1.0.0' - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -3167,14 +3203,6 @@ packages: '@babel/core': 7.29.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} @@ -3303,11 +3331,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} @@ -3463,12 +3486,6 @@ packages: peerDependencies: '@babel/core': 7.29.0 - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-typescript@7.28.6': resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} @@ -3888,14 +3905,6 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -3904,10 +3913,6 @@ packages: resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} @@ -3916,10 +3921,6 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -9447,6 +9448,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pusher-js@8.5.0: + resolution: {integrity: sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==} + qrcode.react@4.2.0: resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} peerDependencies: @@ -9922,10 +9926,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - sax@1.5.0: - resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} - engines: {node: '>=11.0.0'} - sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -10592,6 +10592,9 @@ packages: resolution: {integrity: sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA==} hasBin: true + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -10752,6 +10755,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -10951,18 +10955,6 @@ packages: utf-8-validate: optional: true - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@7.5.11: resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} engines: {node: '>=8.3.0'} @@ -11106,12 +11098,6 @@ snapshots: axe-core: 4.11.1 playwright-core: 1.60.0 - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -11122,15 +11108,15 @@ snapshots: '@babel/core@7.29.0': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -11148,22 +11134,6 @@ snapshots: eslint-visitor-keys: 2.1.0 semver: 6.3.1 - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/generator@7.29.7': dependencies: '@babel/parser': 7.29.7 @@ -11205,7 +11175,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -11224,7 +11194,7 @@ snapshots: '@babel/helper-plugin-utils': 7.28.6 debug: 4.4.3 lodash.debounce: 4.0.8 - resolve: 1.22.11 + resolve: 1.22.12 transitivePeerDependencies: - supports-color @@ -11234,22 +11204,22 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -11257,14 +11227,14 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.29.7 '@babel/helper-plugin-utils@7.27.1': {} @@ -11275,7 +11245,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.28.3 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11284,7 +11254,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11293,14 +11263,14 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -11316,25 +11286,21 @@ snapshots: '@babel/helper-wrap-function@7.28.3': dependencies: - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helpers@7.28.6': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 '@babel/parser@7.28.4': dependencies: '@babel/types': 7.28.4 - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 @@ -11343,7 +11309,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11370,7 +11336,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11386,22 +11352,22 @@ snapshots: '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': dependencies: @@ -11426,67 +11392,62 @@ snapshots: '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: @@ -11509,7 +11470,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11553,10 +11514,10 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-globals': 7.28.0 + '@babel/helper-globals': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11564,13 +11525,13 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/template': 7.28.6 + '@babel/template': 7.29.7 '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11633,7 +11594,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11678,8 +11639,8 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11719,7 +11680,7 @@ snapshots: '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.0) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -11800,7 +11761,7 @@ snapshots: '@babel/helper-module-imports': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -11987,7 +11948,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 esutils: 2.0.3 '@babel/preset-react@7.27.1(@babel/core@7.29.0)': @@ -12028,18 +11989,6 @@ snapshots: '@babel/runtime@7.29.7': {} - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -12048,24 +11997,12 @@ snapshots: '@babel/traverse@7.28.4': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.28.3 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -12087,11 +12024,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -12239,7 +12171,7 @@ snapshots: '@commitlint/is-ignored@19.8.1': dependencies: '@commitlint/types': 19.8.1 - semver: 7.7.4 + semver: 7.8.1 '@commitlint/lint@19.8.1': dependencies: @@ -12821,7 +12753,7 @@ snapshots: identity-obj-proxy: 3.0.0 jasmine: 3.99.0 jasmine-core: 3.99.1 - jest: 29.7.0(@types/node@24.12.4) + jest: 29.7.0(@types/node@24.12.4)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)) jest-environment-jsdom: 29.7.0 jest-jasmine2: 29.7.0 jest-junit: 13.2.0 @@ -12834,7 +12766,7 @@ snapshots: postcss-url: 10.1.3(postcss@8.5.6) react-test-renderer: 19.2.4(react@18.3.1) recursive-copy: 2.0.14 - resolve: 1.22.11 + resolve: 1.22.12 rollup: 3.29.5 rollup-plugin-clear: 2.0.7 rollup-plugin-command: 1.1.3 @@ -12843,7 +12775,7 @@ snapshots: rollup-plugin-postcss: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)) rollup-plugin-re: 1.0.7 sass: 1.93.2 - semver: 7.7.4 + semver: 7.8.1 shelljs: 0.8.5 shx: 0.3.4 ts-jest: 29.4.5(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@29.7.0(@types/node@24.12.4)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)))(typescript@5.9.3) @@ -13150,7 +13082,7 @@ snapshots: '@react-native/babel-plugin-codegen@0.77.3(@babel/preset-env@7.28.3(@babel/core@7.29.0))': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 '@react-native/codegen': 0.77.3(@babel/preset-env@7.28.3(@babel/core@7.29.0)) transitivePeerDependencies: - '@babel/preset-env' @@ -13198,7 +13130,7 @@ snapshots: '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/template': 7.28.6 + '@babel/template': 7.29.7 '@react-native/babel-plugin-codegen': 0.77.3(@babel/preset-env@7.28.3(@babel/core@7.29.0)) babel-plugin-syntax-hermes-parser: 0.25.1 babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) @@ -13209,7 +13141,7 @@ snapshots: '@react-native/codegen@0.77.3(@babel/preset-env@7.28.3(@babel/core@7.29.0))': dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.7 '@babel/preset-env': 7.28.3(@babel/core@7.29.0) glob: 7.2.3 hermes-parser: 0.25.1 @@ -13336,7 +13268,7 @@ snapshots: '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 - resolve: 1.22.11 + resolve: 1.22.12 optionalDependencies: rollup: 3.29.5 @@ -13368,7 +13300,7 @@ snapshots: '@rollup/plugin-typescript@12.1.4(rollup@3.29.5)(tslib@2.8.1)(typescript@5.9.3)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@3.29.5) - resolve: 1.22.11 + resolve: 1.22.12 typescript: 5.9.3 optionalDependencies: rollup: 3.29.5 @@ -13465,8 +13397,8 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.6 + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -13477,7 +13409,7 @@ snapshots: '@testing-library/jest-dom@5.17.0': dependencies: '@adobe/css-tools': 4.4.4 - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.7 '@types/testing-library__jest-dom': 5.14.9 aria-query: 5.3.2 chalk: 3.0.0 @@ -13496,11 +13428,11 @@ snapshots: react-test-renderer: 19.2.4(react@18.3.1) redent: 3.0.0 optionalDependencies: - jest: 29.7.0(@types/node@24.12.4) + jest: 29.7.0(@types/node@24.12.4)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)) '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -13560,24 +13492,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.29.7 '@types/big.js@6.2.2': {} @@ -13863,7 +13795,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.57.0 debug: 4.4.3 minimatch: 10.2.4 - semver: 7.7.4 + semver: 7.8.1 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 @@ -14322,7 +14254,7 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -14332,8 +14264,8 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 @@ -14535,8 +14467,8 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.26.3 - caniuse-lite: 1.0.30001750 + browserslist: 4.28.1 + caniuse-lite: 1.0.30001778 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -15438,7 +15370,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -15521,7 +15453,7 @@ snapshots: es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 es-to-primitive@1.3.0: dependencies: @@ -16116,7 +16048,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 - hasown: 2.0.2 + hasown: 2.0.4 is-callable: 1.2.7 functions-have-names@1.2.3: {} @@ -16584,7 +16516,7 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.2 + hasown: 2.0.4 side-channel: 1.1.0 interpret@1.4.0: {} @@ -16799,7 +16731,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.29.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -16809,10 +16741,10 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.29.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color @@ -17066,7 +16998,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -17078,7 +17010,7 @@ snapshots: jest-message-util@30.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 30.2.0 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -17123,7 +17055,7 @@ snapshots: jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 jest-validate: 29.7.0 - resolve: 1.22.11 + resolve: 1.22.12 resolve.exports: 2.0.3 slash: 3.0.0 @@ -17183,10 +17115,10 @@ snapshots: jest-snapshot@29.7.0: dependencies: '@babel/core': 7.29.0 - '@babel/generator': 7.28.3 + '@babel/generator': 7.29.7 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) - '@babel/types': 7.28.4 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.7 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 @@ -17201,7 +17133,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.4 + semver: 7.8.1 transitivePeerDependencies: - supports-color @@ -17256,18 +17188,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@24.12.4): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.12.4)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@29.7.0(@types/node@24.12.4)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)) @@ -17312,7 +17232,7 @@ snapshots: jscodeshift@17.3.0(@babel/preset-env@7.28.3(@babel/core@7.29.0)): dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.7 '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.29.0) @@ -17479,7 +17399,7 @@ snapshots: chokidar: 3.6.0 livereload-js: 3.4.1 opts: 2.0.2 - ws: 7.5.10 + ws: 7.5.11 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -17593,7 +17513,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.1 make-error@1.3.6: {} @@ -18056,7 +17976,7 @@ snapshots: node-abi@3.78.0: dependencies: - semver: 7.7.4 + semver: 7.8.1 optional: true node-addon-api@7.1.1: @@ -18279,7 +18199,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -18449,7 +18369,7 @@ snapshots: postcss-colormin@5.3.1(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 caniuse-api: 3.0.0 colord: 2.9.3 postcss: 8.5.6 @@ -18457,7 +18377,7 @@ snapshots: postcss-convert-values@5.1.3(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -18482,7 +18402,7 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.11 + resolve: 1.22.12 postcss-import@16.1.1(postcss@8.5.6): dependencies: @@ -18507,7 +18427,7 @@ snapshots: postcss-merge-rules@5.1.4(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 caniuse-api: 3.0.0 cssnano-utils: 3.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -18527,7 +18447,7 @@ snapshots: postcss-minify-params@5.1.4(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 cssnano-utils: 3.1.0(postcss@8.5.6) postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -18601,7 +18521,7 @@ snapshots: postcss-normalize-unicode@5.1.1(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -18624,7 +18544,7 @@ snapshots: postcss-reduce-initial@5.1.2(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 caniuse-api: 3.0.0 postcss: 8.5.6 @@ -18798,6 +18718,10 @@ snapshots: pure-rand@6.1.0: {} + pusher-js@8.5.0: + dependencies: + tweetnacl: 1.0.3 + qrcode.react@4.2.0(react@18.3.1): dependencies: react: 18.3.1 @@ -19303,7 +19227,7 @@ snapshots: resolve@1.22.11: dependencies: - is-core-module: 2.16.1 + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -19489,8 +19413,6 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 - sax@1.5.0: {} - sax@1.6.0: {} saxes@6.0.0: @@ -19919,7 +19841,7 @@ snapshots: stylehacks@5.1.1(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 postcss: 8.5.6 postcss-selector-parser: 6.1.2 @@ -20118,11 +20040,11 @@ snapshots: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@24.12.4) + jest: 29.7.0(@types/node@24.12.4)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.12.4)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.4 + semver: 7.8.1 type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 @@ -20206,6 +20128,8 @@ snapshots: turbo-windows-64: 2.8.16 turbo-windows-arm64: 2.8.16 + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -20602,8 +20526,6 @@ snapshots: dependencies: async-limiter: 1.0.1 - ws@7.5.10: {} - ws@7.5.11: {} ws@8.18.3: {} @@ -20614,7 +20536,7 @@ snapshots: xml2js@0.6.2: dependencies: - sax: 1.5.0 + sax: 1.6.0 xmlbuilder: 11.0.1 xml@1.0.1: {} From cec035e372d1e5a9320d33a2ac40416894e82bb4 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 10:47:56 +0200 Subject: [PATCH 02/11] fix(pusher-web): depend on enable to react on config changes --- .../pusher-web/src/hooks/usePusherListener.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts index 193011f20c..4a269c017e 100644 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts @@ -37,7 +37,7 @@ export function usePusherListener(subscription?: SubscriptionConfig): void { // Subscribe/unsubscribe based on subscription config changes useEffect(() => { const listener = listenerRef.current; - if (!listener || !subscription) { + if (!enabled || !listener) { return; } @@ -47,5 +47,5 @@ export function usePusherListener(subscription?: SubscriptionConfig): void { return () => { listener.unsubscribe(); }; - }, [subscription]); + }, [enabled, subscription]); } From ffd5538f96e64527002db5c2ab683629bf6a435a Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 11:05:56 +0200 Subject: [PATCH 03/11] fix(pusher-web): abort key fetching on unmount --- .../pusher-web/src/hooks/usePusherConfig.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts index b20b53c84a..6637657031 100644 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts @@ -14,6 +14,8 @@ export function usePusherConfig(): PusherConfig | null { const [config, setConfig] = useState(null); useEffect(() => { + let active = true; + const controller = new AbortController(); const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; const endpoint = `${baseUrl}rest/pusher/key`; const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; @@ -25,7 +27,8 @@ export function usePusherConfig(): PusherConfig | null { credentials: "same-origin", headers: { "X-Csrf-Token": csrfToken - } + }, + signal: controller.signal }) .then(response => { if (response.status !== 200) { @@ -34,6 +37,9 @@ export function usePusherConfig(): PusherConfig | null { return response.text(); }) .then(data => { + if (!active) { + return; + } const keyData = JSON.parse(data) as KeyData; if (!keyData.key || !keyData.cluster) { throw new Error("Invalid Pusher key data: missing key or cluster"); @@ -46,9 +52,17 @@ export function usePusherConfig(): PusherConfig | null { }); }) .catch(error => { + if (!active || (error instanceof DOMException && error.name === "AbortError")) { + return; + } console.error("[usePusherConfig] Failed to fetch Pusher configuration:", error); setConfig(null); }); + + return () => { + active = false; + controller.abort(); + }; }, []); return config; From b0a7e747917f89cc22bae7db46e1c79deb30e7ff Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 11:17:05 +0200 Subject: [PATCH 04/11] chore(pusher-web): rename properties --- .../pusher-web/src/Pusher.editorConfig.ts | 2 +- packages/pluggableWidgets/pusher-web/src/Pusher.tsx | 10 +++++----- packages/pluggableWidgets/pusher-web/src/Pusher.xml | 8 ++++---- .../pusher-web/typings/PusherProps.d.ts | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts index 1a7e31068b..ff85ea8768 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts @@ -27,5 +27,5 @@ export function getCustomCaption(values: PusherPreviewProps): string { } export function getCaption(values: PusherPreviewProps): string { - return `Pusher widget [${values.notifyChannelName}]`; + return `Pusher widget [${values.notifyActionName}]`; } diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx index 3c617a0c44..ea7bcd9e99 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -7,7 +7,7 @@ import "./ui/Pusher.scss"; import { useMxObjectInfo } from "./utils/useMxObjectInfo"; export default function Pusher(props: PusherContainerProps): ReactElement { - const { class: className, objectSource, notifyChannelName, notifyAction } = props; + const { class: className, objectSource, notifyActionName, notifyEventAction } = props; // Extract object GUID and entity name from data source const mxObjectInfo = useMxObjectInfo(objectSource as any); // TODO: fix typings when PWT updated. @@ -18,9 +18,9 @@ export default function Pusher(props: PusherContainerProps): ReactElement { console.debug("[Pusher] Event received:", data); // Execute configured action - executeAction(notifyAction); + executeAction(notifyEventAction); }, - [notifyAction] + [notifyEventAction] ); // Error callback @@ -37,11 +37,11 @@ export default function Pusher(props: PusherContainerProps): ReactElement { return { entityName: mxObjectInfo.entityName, guid: mxObjectInfo.guid, - eventName: notifyChannelName, + eventName: notifyActionName, onEvent: handleEvent, onError: handleError }; - }, [mxObjectInfo, handleEvent, handleError, notifyChannelName]); + }, [mxObjectInfo, handleEvent, handleError, notifyActionName]); // Initialize Pusher listener usePusherListener(subscription); diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.xml b/packages/pluggableWidgets/pusher-web/src/Pusher.xml index ba46794ffd..f17f0a8f7a 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.xml +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.xml @@ -10,12 +10,12 @@ - - Notify event name - The name should match the with the 'Notify' parameter `EventName` + + Notify action name + The name should match the with the 'Notify' parameter `ActionName` - + Action diff --git a/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts index 1db883cffb..66aa96b78a 100644 --- a/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts +++ b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts @@ -12,8 +12,8 @@ export interface PusherContainerProps { style?: CSSProperties; tabIndex?: number; objectSource: ListValue; - notifyChannelName: string; - notifyAction?: ActionValue; + notifyActionName: string; + notifyEventAction?: ActionValue; } export interface PusherPreviewProps { @@ -28,6 +28,6 @@ export interface PusherPreviewProps { renderMode: "design" | "xray" | "structure"; translate: (text: string) => string; objectSource: {} | { caption: string } | { type: string } | null; - notifyChannelName: string; - notifyAction: {} | null; + notifyActionName: string; + notifyEventAction: {} | null; } From 5464d2a257f20038e025a054a7fff2fc57dc1010 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 12:07:49 +0200 Subject: [PATCH 05/11] chore(pusher-web): reorganize hooks --- .../pusher-web/src/Pusher.tsx | 4 +- .../src/hooks/useFetchPusherConfig.ts | 29 ++++++++ .../pusher-web/src/hooks/usePusherConfig.ts | 69 ------------------- .../pusher-web/src/hooks/usePusherListener.ts | 60 ++++++---------- .../src/hooks/usePusherSubscribe.ts | 21 ++++++ .../pusher-web/src/utils/PusherListener.ts | 2 +- .../pusher-web/src/utils/fetchPusherConfig.ts | 53 ++++++++++++++ 7 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts delete mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx index ea7bcd9e99..8269158b43 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -2,7 +2,7 @@ import classnames from "classnames"; import { ReactElement, useCallback, useMemo } from "react"; import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; import { PusherContainerProps } from "../typings/PusherProps"; -import { usePusherListener } from "./hooks/usePusherListener"; +import { usePusherSubscribe } from "./hooks/usePusherSubscribe"; import "./ui/Pusher.scss"; import { useMxObjectInfo } from "./utils/useMxObjectInfo"; @@ -44,7 +44,7 @@ export default function Pusher(props: PusherContainerProps): ReactElement { }, [mxObjectInfo, handleEvent, handleError, notifyActionName]); // Initialize Pusher listener - usePusherListener(subscription); + usePusherSubscribe(subscription); return
; } diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts new file mode 100644 index 0000000000..660220c56c --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { fetchPusherConfig } from "../utils/fetchPusherConfig"; +import { PusherConfig } from "../utils/PusherListener"; + +/** + * Provides Pusher configuration fetched from the backend. + * Returns null while loading or on error. + */ +export function useFetchPusherConfig(): PusherConfig | null { + const [config, setConfig] = useState(null); + + useEffect(() => { + let active = true; + const controller = new AbortController(); + + fetchPusherConfig(controller.signal).then(result => { + if (active) { + setConfig(result); + } + }); + + return () => { + active = false; + controller.abort(); + }; + }, []); + + return config; +} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts deleted file mode 100644 index 6637657031..0000000000 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useEffect, useState } from "react"; -import { PusherConfig } from "../utils/PusherListener"; - -interface KeyData { - key: string; - cluster: string; -} - -/** - * Fetch Pusher configuration from backend - * Returns null while loading or on error - */ -export function usePusherConfig(): PusherConfig | null { - const [config, setConfig] = useState(null); - - useEffect(() => { - let active = true; - const controller = new AbortController(); - const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; - const endpoint = `${baseUrl}rest/pusher/key`; - const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; - - const authEndpoint = `${baseUrl}rest/pusher/auth`; - - fetch(endpoint, { - method: "GET", - credentials: "same-origin", - headers: { - "X-Csrf-Token": csrfToken - }, - signal: controller.signal - }) - .then(response => { - if (response.status !== 200) { - throw new Error(`Failed to fetch Pusher key: HTTP ${response.status}`); - } - return response.text(); - }) - .then(data => { - if (!active) { - return; - } - const keyData = JSON.parse(data) as KeyData; - if (!keyData.key || !keyData.cluster) { - throw new Error("Invalid Pusher key data: missing key or cluster"); - } - setConfig({ - key: keyData.key, - cluster: keyData.cluster, - authEndpoint, - csrfToken - }); - }) - .catch(error => { - if (!active || (error instanceof DOMException && error.name === "AbortError")) { - return; - } - console.error("[usePusherConfig] Failed to fetch Pusher configuration:", error); - setConfig(null); - }); - - return () => { - active = false; - controller.abort(); - }; - }, []); - - return config; -} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts index 4a269c017e..d91d7c4ecf 100644 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts @@ -1,51 +1,35 @@ -import { useEffect, useRef } from "react"; -import { usePusherConfig } from "./usePusherConfig"; -import { PusherListener, SubscriptionConfig } from "../utils/PusherListener"; +import { useEffect, useRef, useState } from "react"; +import { useFetchPusherConfig } from "./useFetchPusherConfig"; +import { PusherListener } from "../utils/PusherListener"; /** - * React hook to manage Pusher listener lifecycle - * Automatically handles initialization, subscription changes, and cleanup + * Creates and initializes a PusherListener */ -export function usePusherListener(subscription?: SubscriptionConfig): void { - const listenerRef = useRef(null); +export function usePusherListener(): PusherListener | null { + const instanceRef = useRef(null); + const [ready, setReady] = useState(false); - // Fetch Pusher config from backend - const pusherConfig = usePusherConfig(); + const pusherConfig = useFetchPusherConfig(); - const enabled = !!pusherConfig && !!subscription; - - // Initialize PusherListener once when config is available useEffect(() => { - if (!enabled) { + if (!pusherConfig) { return; } - - const listener = new PusherListener(pusherConfig); - listenerRef.current = listener; - - listener.initialize().catch(error => { - console.error("[usePusherListener] Failed to initialize:", error); - }); - - // Cleanup on unmount or when config changes - return () => { - listener.destroy(); - listenerRef.current = null; - }; - }, [pusherConfig, enabled]); - - // Subscribe/unsubscribe based on subscription config changes - useEffect(() => { - const listener = listenerRef.current; - if (!enabled || !listener) { + try { + const instance = new PusherListener(pusherConfig); + instance.initialize(); + instanceRef.current = instance; + setReady(true); + } catch (error) { + console.error("[usePusherListenerInstance] Failed to initialize:", error); return; } - - listener.subscribe(subscription); - - // Unsubscribe on cleanup or when subscription changes return () => { - listener.unsubscribe(); + instanceRef.current?.destroy(); + instanceRef.current = null; + setReady(false); }; - }, [enabled, subscription]); + }, [pusherConfig]); + + return ready ? instanceRef.current : null; } diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts new file mode 100644 index 0000000000..52841e4f25 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts @@ -0,0 +1,21 @@ +import { useEffect } from "react"; +import { usePusherListener } from "./usePusherListener"; +import { SubscriptionConfig } from "../utils/PusherListener"; + +/** + * Manages the full Pusher listener lifecycle: config fetching, initialization, + * and subscription. Resubscribes automatically when subscription changes. + */ +export function usePusherSubscribe(subscription?: SubscriptionConfig): void { + const listener = usePusherListener(); + + useEffect(() => { + if (!listener || !subscription) { + return; + } + listener.subscribe(subscription); + return () => { + listener.unsubscribe(); + }; + }, [listener, subscription]); +} diff --git a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts index 0da56ed040..0f7d3cda10 100644 --- a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts @@ -27,7 +27,7 @@ export class PusherListener { * Initialize Pusher connection * Should be called once on widget mount */ - async initialize(): Promise { + initialize(): void { if (this.pusher) { return; // Already initialized } diff --git a/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts new file mode 100644 index 0000000000..ed97fbe650 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts @@ -0,0 +1,53 @@ +import { PusherConfig } from "./PusherListener"; + +interface KeyData { + key: string; + cluster: string; +} + +/** + * Fetches Pusher configuration from the backend. + * Returns a PusherConfig on success, or null on error / invalid response. + */ +export async function fetchPusherConfig(signal: AbortSignal): Promise { + const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; + const endpoint = `${baseUrl}rest/pusher/key`; + const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; + const authEndpoint = `${baseUrl}rest/pusher/auth`; + + let response: Response; + try { + response = await fetch(endpoint, { + method: "GET", + credentials: "same-origin", + headers: { "X-Csrf-Token": csrfToken }, + signal + }); + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + return null; + } + console.error("[fetchPusherConfig] Network error:", error); + return null; + } + + if (response.status !== 200) { + console.error(`[fetchPusherConfig] Unexpected response: HTTP ${response.status}`); + return null; + } + + let keyData: KeyData; + try { + keyData = JSON.parse(await response.text()) as KeyData; + } catch (error) { + console.error("[fetchPusherConfig] Failed to parse response:", error); + return null; + } + + if (!keyData.key || !keyData.cluster) { + console.error("[fetchPusherConfig] Invalid response: missing key or cluster"); + return null; + } + + return { key: keyData.key, cluster: keyData.cluster, authEndpoint, csrfToken }; +} From c0d256dc7fecb5e96f4dac4f37e816c66373f382 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 15:23:02 +0200 Subject: [PATCH 06/11] chore(pusher-web): mark as private --- packages/pluggableWidgets/pusher-web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pluggableWidgets/pusher-web/package.json b/packages/pluggableWidgets/pusher-web/package.json index 21b64c822e..a28408cd8c 100644 --- a/packages/pluggableWidgets/pusher-web/package.json +++ b/packages/pluggableWidgets/pusher-web/package.json @@ -5,6 +5,7 @@ "description": "Pusher.com integration widget for real-time communication", "copyright": "© Mendix Technology BV 2026. All rights reserved.", "license": "Apache-2.0", + "private": true, "repository": { "type": "git", "url": "https://github.com/mendix/web-widgets.git" From 64a2d6c64da6ec8d08d652f767e1d04c777db230 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Tue, 26 May 2026 13:30:10 +0200 Subject: [PATCH 07/11] chore: add stub test --- .../pusher-web/src/__tests__/Pusher.spec.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx b/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx index aafb61a263..1024b5d20e 100644 --- a/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx +++ b/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx @@ -1,7 +1,10 @@ describe("Pusher", () => { - // TODO: Add comprehensive unit tests for: - // - PusherListener class (connection, subscription, cleanup) - // - usePusherConfig hook (fetching config) - // - usePusherListener hook (React lifecycle integration) - // - Event handling and action execution + it("placeholder – tests to be implemented", () => { + // TODO: Add comprehensive unit tests for: + // - PusherListener class (connection, subscription, cleanup) + // - usePusherConfig hook (fetching config) + // - usePusherListener hook (React lifecycle integration) + // - Event handling and action execution + expect(true).toBe(true); + }); }); From 884f36361be89907d437b5a8fa3d1e4d2f6aa6f1 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Tue, 26 May 2026 14:53:43 +0200 Subject: [PATCH 08/11] chore: better url handling --- .../pusher-web/src/utils/fetchPusherConfig.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts index ed97fbe650..7e067b7b1a 100644 --- a/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts +++ b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts @@ -10,14 +10,13 @@ interface KeyData { * Returns a PusherConfig on success, or null on error / invalid response. */ export async function fetchPusherConfig(signal: AbortSignal): Promise { - const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; - const endpoint = `${baseUrl}rest/pusher/key`; - const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; - const authEndpoint = `${baseUrl}rest/pusher/auth`; + const keyEndpoint = getMendixUrl("rest/pusher/key"); + const authEndpoint = getMendixUrl("rest/pusher/auth"); + const csrfToken = getCsrfToken(); let response: Response; try { - response = await fetch(endpoint, { + response = await fetch(keyEndpoint, { method: "GET", credentials: "same-origin", headers: { "X-Csrf-Token": csrfToken }, @@ -51,3 +50,12 @@ export async function fetchPusherConfig(signal: AbortSignal): Promise Date: Thu, 4 Jun 2026 17:15:50 +0200 Subject: [PATCH 09/11] chore: improve subscription logic --- packages/pluggableWidgets/pusher-web/src/Pusher.tsx | 2 +- .../pusher-web/src/utils/PusherListener.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx index 8269158b43..6b7a2a30c9 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -41,7 +41,7 @@ export default function Pusher(props: PusherContainerProps): ReactElement { onEvent: handleEvent, onError: handleError }; - }, [mxObjectInfo, handleEvent, handleError, notifyActionName]); + }, [mxObjectInfo, notifyActionName, handleEvent, handleError]); // Initialize Pusher listener usePusherSubscribe(subscription); diff --git a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts index 0f7d3cda10..1ede86bc22 100644 --- a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts @@ -90,10 +90,8 @@ export class PusherListener { */ unsubscribe(): void { if (this.currentChannel && this.currentChannelName) { - // Unbind event handler before unsubscribing - if (this.currentEventName) { - this.currentChannel.unbind(this.currentEventName); - } + // Unbind all channel events + this.currentChannel.unbind(); this.pusher?.unsubscribe(this.currentChannelName); this.currentChannel = null; this.currentChannelName = null; @@ -108,6 +106,7 @@ export class PusherListener { destroy(): void { this.unsubscribe(); if (this.pusher) { + this.pusher.connection.unbind(); this.pusher.disconnect(); this.pusher = null; } From 7bba5cd2736a2d56db216d426b8dfd5be2c83991 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 5 Jun 2026 16:56:24 +0200 Subject: [PATCH 10/11] chore: rework subscription flow --- .../pusher-web/src/Pusher.tsx | 16 +++--- .../src/hooks/useFetchPusherConfig.ts | 29 ----------- .../pusher-web/src/hooks/usePusherListener.ts | 35 ------------- .../src/hooks/usePusherSubscribe.ts | 40 ++++++++++++--- .../pusher-web/src/utils/PusherListener.ts | 51 +++++-------------- .../src/utils/createPusherListener.ts | 14 +++++ .../pusher-web/src/utils/fetchPusherConfig.ts | 2 +- .../{useMxObjectInfo.ts => getChannelName.ts} | 26 ++++------ 8 files changed, 77 insertions(+), 136 deletions(-) delete mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts delete mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/utils/createPusherListener.ts rename packages/pluggableWidgets/pusher-web/src/utils/{useMxObjectInfo.ts => getChannelName.ts} (55%) diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx index 6b7a2a30c9..1002c252a8 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -4,14 +4,11 @@ import { executeAction } from "@mendix/widget-plugin-platform/framework/execute- import { PusherContainerProps } from "../typings/PusherProps"; import { usePusherSubscribe } from "./hooks/usePusherSubscribe"; import "./ui/Pusher.scss"; -import { useMxObjectInfo } from "./utils/useMxObjectInfo"; +import { getChannelName } from "./utils/getChannelName"; export default function Pusher(props: PusherContainerProps): ReactElement { const { class: className, objectSource, notifyActionName, notifyEventAction } = props; - // Extract object GUID and entity name from data source - const mxObjectInfo = useMxObjectInfo(objectSource as any); // TODO: fix typings when PWT updated. - // Event callback - triggered when Pusher event is received const handleEvent = useCallback( (data: unknown) => { @@ -28,22 +25,23 @@ export default function Pusher(props: PusherContainerProps): ReactElement { console.error("[Pusher] Subscription error:", error.message); }, []); + // Build channel name based on the object + const channelName = getChannelName(objectSource as any); // TODO: fix typings when PWT updated. + // Setup stable subscription config const subscription = useMemo(() => { - if (!mxObjectInfo) { + if (!channelName) { return undefined; } return { - entityName: mxObjectInfo.entityName, - guid: mxObjectInfo.guid, + channelName, eventName: notifyActionName, onEvent: handleEvent, onError: handleError }; - }, [mxObjectInfo, notifyActionName, handleEvent, handleError]); + }, [channelName, notifyActionName, handleEvent, handleError]); - // Initialize Pusher listener usePusherSubscribe(subscription); return
; diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts deleted file mode 100644 index 660220c56c..0000000000 --- a/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useState } from "react"; -import { fetchPusherConfig } from "../utils/fetchPusherConfig"; -import { PusherConfig } from "../utils/PusherListener"; - -/** - * Provides Pusher configuration fetched from the backend. - * Returns null while loading or on error. - */ -export function useFetchPusherConfig(): PusherConfig | null { - const [config, setConfig] = useState(null); - - useEffect(() => { - let active = true; - const controller = new AbortController(); - - fetchPusherConfig(controller.signal).then(result => { - if (active) { - setConfig(result); - } - }); - - return () => { - active = false; - controller.abort(); - }; - }, []); - - return config; -} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts deleted file mode 100644 index d91d7c4ecf..0000000000 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useFetchPusherConfig } from "./useFetchPusherConfig"; -import { PusherListener } from "../utils/PusherListener"; - -/** - * Creates and initializes a PusherListener - */ -export function usePusherListener(): PusherListener | null { - const instanceRef = useRef(null); - const [ready, setReady] = useState(false); - - const pusherConfig = useFetchPusherConfig(); - - useEffect(() => { - if (!pusherConfig) { - return; - } - try { - const instance = new PusherListener(pusherConfig); - instance.initialize(); - instanceRef.current = instance; - setReady(true); - } catch (error) { - console.error("[usePusherListenerInstance] Failed to initialize:", error); - return; - } - return () => { - instanceRef.current?.destroy(); - instanceRef.current = null; - setReady(false); - }; - }, [pusherConfig]); - - return ready ? instanceRef.current : null; -} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts index 52841e4f25..fa53f55d63 100644 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts @@ -1,18 +1,44 @@ -import { useEffect } from "react"; -import { usePusherListener } from "./usePusherListener"; -import { SubscriptionConfig } from "../utils/PusherListener"; +import { useEffect, useState } from "react"; +import { createPusherListener } from "../utils/createPusherListener"; +import { PusherListener, SubscriptionConfig } from "../utils/PusherListener"; /** - * Manages the full Pusher listener lifecycle: config fetching, initialization, - * and subscription. Resubscribes automatically when subscription changes. + * Manages the full Pusher lifecycle: fetches config, creates the listener + * instance, and manages the channel subscription. + * Resubscribes automatically when the subscription config changes. */ export function usePusherSubscribe(subscription?: SubscriptionConfig): void { - const listener = usePusherListener(); + const [listener, setListener] = useState(null); useEffect(() => { - if (!listener || !subscription) { + const controller = new AbortController(); + let instance: PusherListener | null = null; + + createPusherListener(controller.signal).then(result => { + if (controller.signal.aborted) { + result?.destroy(); + return; + } + instance = result; + setListener(result); + }); + + return () => { + controller.abort(); + instance?.destroy(); + setListener(null); + }; + }, []); + + useEffect(() => { + if (!listener) { return; } + if (!subscription) { + listener.unsubscribe(); + return; + } + listener.subscribe(subscription); return () => { listener.unsubscribe(); diff --git a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts index 1ede86bc22..845d34bbf7 100644 --- a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts @@ -8,30 +8,18 @@ export interface PusherConfig { } export interface SubscriptionConfig { - entityName: string; - guid: string; + channelName: string; eventName: string; onEvent: (data: unknown) => void; onError?: (error: Error) => void; } export class PusherListener { - private pusher: Pusher | null = null; + private pusher: Pusher; private currentChannel: Channel | null = null; - private currentChannelName: string | null = null; - private currentEventName: string | null = null; - - constructor(private config: PusherConfig) {} - - /** - * Initialize Pusher connection - * Should be called once on widget mount - */ - initialize(): void { - if (this.pusher) { - return; // Already initialized - } + private currentSubscription: SubscriptionConfig | null = null; + constructor(private config: PusherConfig) { this.pusher = new Pusher(this.config.key, { cluster: this.config.cluster, authEndpoint: this.config.authEndpoint, @@ -52,14 +40,8 @@ export class PusherListener { * Automatically unsubscribes from previous channel if different */ subscribe(config: SubscriptionConfig): void { - if (!this.pusher) { - throw new Error("PusherListener not initialized. Call initialize() first."); - } - - const channelName = this.buildChannelName(config.entityName, config.guid); - // If already subscribed to same channel and event, do nothing - if (channelName === this.currentChannelName && config.eventName === this.currentEventName) { + if (config === this.currentSubscription) { return; } @@ -67,9 +49,8 @@ export class PusherListener { this.unsubscribe(); // Subscribe to new channel - this.currentChannelName = channelName; - this.currentEventName = config.eventName; - this.currentChannel = this.pusher.subscribe(channelName); + this.currentSubscription = config; + this.currentChannel = this.pusher.subscribe(config.channelName); // Bind event handler this.currentChannel.bind(config.eventName, config.onEvent); @@ -89,13 +70,12 @@ export class PusherListener { * Unsubscribe from current channel */ unsubscribe(): void { - if (this.currentChannel && this.currentChannelName) { + if (this.currentChannel && this.currentSubscription) { // Unbind all channel events this.currentChannel.unbind(); - this.pusher?.unsubscribe(this.currentChannelName); + this.pusher.unsubscribe(this.currentSubscription.channelName); this.currentChannel = null; - this.currentChannelName = null; - this.currentEventName = null; + this.currentSubscription = null; } } @@ -105,15 +85,8 @@ export class PusherListener { */ destroy(): void { this.unsubscribe(); - if (this.pusher) { - this.pusher.connection.unbind(); - this.pusher.disconnect(); - this.pusher = null; - } - } - - private buildChannelName(entityName: string, guid: string): string { - return `private-${entityName}.${guid}`; + this.pusher.connection.unbind(); + this.pusher.disconnect(); } private handleConnectionError = (error: unknown): void => { diff --git a/packages/pluggableWidgets/pusher-web/src/utils/createPusherListener.ts b/packages/pluggableWidgets/pusher-web/src/utils/createPusherListener.ts new file mode 100644 index 0000000000..912130f815 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/utils/createPusherListener.ts @@ -0,0 +1,14 @@ +import { fetchPusherConfig } from "./fetchPusherConfig"; +import { PusherListener } from "./PusherListener"; + +/** + * Fetches Pusher configuration and creates a ready-to-use PusherListener. + * Returns null if the config fetch fails or the request is aborted. + */ +export async function createPusherListener(signal: AbortSignal): Promise { + const config = await fetchPusherConfig(signal); + if (!config) { + return null; + } + return new PusherListener(config); +} diff --git a/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts index 7e067b7b1a..388bffc3d4 100644 --- a/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts +++ b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts @@ -37,7 +37,7 @@ export async function fetchPusherConfig(signal: AbortSignal): Promise): MxObjectInfo | undefined { +export function getChannelName(objectSource: DynamicValue): string | undefined { const object = (objectSource as any)?.value as ObjectItem | undefined; const guid = object?.id; const entityName = object ? extractEntityName(object) : undefined; - return useMemo(() => { - if (!guid || !entityName) { - return undefined; - } - return { - guid, - entityName - }; - }, [guid, entityName]); + if (!guid || !entityName) { + return undefined; + } + + return buildChannelName(entityName, guid); } function extractEntityName(object: ObjectItem): string { @@ -30,3 +20,7 @@ function extractEntityName(object: ObjectItem): string { } return mxObj.getEntity(); } + +function buildChannelName(entityName: string, guid: string): string { + return `private-${entityName}.${guid}`; +} From b4d24d171ffec35b0031b6b751c600763051a511 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 5 Jun 2026 16:59:22 +0200 Subject: [PATCH 11/11] fix: remove unnecessary casting --- .../pusher-web/src/utils/getChannelName.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/utils/getChannelName.ts b/packages/pluggableWidgets/pusher-web/src/utils/getChannelName.ts index 4f4f72cf98..97c93d8a90 100644 --- a/packages/pluggableWidgets/pusher-web/src/utils/getChannelName.ts +++ b/packages/pluggableWidgets/pusher-web/src/utils/getChannelName.ts @@ -1,10 +1,14 @@ import { DynamicValue, ObjectItem } from "mendix"; export function getChannelName(objectSource: DynamicValue): string | undefined { - const object = (objectSource as any)?.value as ObjectItem | undefined; + const object = objectSource.value as ObjectItem | undefined; - const guid = object?.id; - const entityName = object ? extractEntityName(object) : undefined; + if (!object) { + return undefined; + } + + const guid = object.id; + const entityName = extractEntityName(object); if (!guid || !entityName) { return undefined;