Skip to content

chore(pusher-web): initial implementation#2224

Open
r0b1n wants to merge 8 commits into
mainfrom
new-pusher-widget
Open

chore(pusher-web): initial implementation#2224
r0b1n wants to merge 8 commits into
mainfrom
new-pusher-widget

Conversation

@r0b1n
Copy link
Copy Markdown
Collaborator

@r0b1n r0b1n commented May 21, 2026

Pull request type


Description

@r0b1n r0b1n requested a review from a team as a code owner May 21, 2026 15:25
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@r0b1n r0b1n force-pushed the new-pusher-widget branch 2 times, most recently from abe155f to 82fedd8 Compare May 26, 2026 11:29
gjulivan
gjulivan previously approved these changes May 26, 2026
Copy link
Copy Markdown
Collaborator

@iobuhov iobuhov left a comment

Choose a reason for hiding this comment

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

Where is module code?

Comment on lines +13 to +16
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`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's use URL constructor?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@github-actions
Copy link
Copy Markdown

AI Code Review

🔶 Changes requested — one or more medium-severity items must be addressed


What was reviewed

File Change
src/Pusher.tsx Main widget component
src/hooks/useFetchPusherConfig.ts Fetches Pusher key/config from backend
src/hooks/usePusherListener.ts Creates and initializes a PusherListener
src/hooks/usePusherSubscribe.ts Manages subscription lifecycle
src/utils/PusherListener.ts Pusher connection/subscription class
src/utils/fetchPusherConfig.ts REST call to get Pusher key/cluster
src/utils/useMxObjectInfo.ts Extracts GUID + entity name from ObjectItem
src/__tests__/Pusher.spec.tsx Unit tests
src/Pusher.xml Widget XML manifest
typings/PusherProps.d.ts Generated typings
src/Pusher.editorConfig.ts Studio Pro design-time config
src/Pusher.editorPreview.tsx Studio Pro preview component
src/ui/Pusher.scss / PusherPreview.css Styles
package.json, tsconfig.json, eslint.config.mjs Package config
CHANGELOG.md Initial entry present

Skipped (out of scope): pnpm-lock.yaml


Findings

🔶 Medium — Stale onEvent callback when channel/event name is unchanged

File: src/utils/PusherListener.ts line 62
Problem: subscribe() returns early when channelName and eventName match the current values, even if onEvent has changed. In Pusher.tsx, handleEvent is created with useCallback and closes over notifyEventAction. If the user reconfigures the action in Studio Pro without changing the channel or event, a new subscription object is produced (because handleEvent changed), usePusherSubscribe re-runs the effect, subscribe() is called again — but the early return means the channel stays bound to the old callback. The new action is silently never executed.
Fix:

subscribe(config: SubscriptionConfig): void {
    if (!this.pusher) { throw new Error("..."); }
    const channelName = this.buildChannelName(config.entityName, config.guid);

    if (channelName === this.currentChannelName && config.eventName === this.currentEventName) {
        // Channel/event unchanged — just rebind the handler in case it changed
        if (this.currentChannel) {
            this.currentChannel.unbind(this.currentEventName!);
            this.currentChannel.bind(config.eventName, config.onEvent);
        }
        return;
    }
    // … rest of the method
}

🔶 Medium — pusher:subscription_error handler accumulates on every re-subscription

File: src/utils/PusherListener.ts line 78
Problem: Each call to subscribe() binds a new anonymous function to pusher:subscription_error. unsubscribe() only calls unbind(this.currentEventName), so the error handler is never removed. On frequent re-subscriptions (e.g., entity changes) multiple error handlers fire for each error, and references accumulate.
Fix: Track the error handler and unbind it in unsubscribe():

private currentErrorHandler: ((error: unknown) => void) | null = null;

// in subscribe():
this.currentErrorHandler = (error: unknown) => { /* … */ };
this.currentChannel.bind("pusher:subscription_error", this.currentErrorHandler);

// in unsubscribe():
if (this.currentErrorHandler) {
    this.currentChannel.unbind("pusher:subscription_error", this.currentErrorHandler);
    this.currentErrorHandler = null;
}

🔶 Medium — Unit tests are a non-functional placeholder

File: src/__tests__/Pusher.spec.tsx line 1
Problem: The test file contains only expect(true).toBe(true). This new widget has real complexity: async config fetching, Pusher connection/subscription lifecycle, React hook cleanup, and ActionValue execution. None of it is covered. Per repo conventions, new features require unit tests.
Fix: At minimum cover:

  • PusherListener: initialize(), subscribe(), unsubscribe(), destroy(), duplicate subscription early-return
  • useFetchPusherConfig: loading state, successful fetch, network error, unmount abort
  • Pusher component: renders without crash, action executed on event, no action when notifyEventAction is undefined

Use @mendix/widget-plugin-test-utils builders for Mendix data mocks; mock pusher-js and fetchPusherConfig.


⚠️ Low — Connection event handlers not removed in destroy()

File: src/utils/PusherListener.ts line 108
Note: destroy() calls this.pusher.disconnect() but never calls this.pusher.connection.unbind("error", ...) or this.pusher.connection.unbind("state_change", ...). Pusher's connection object is shared; unbound handlers keep references alive until GC. Add explicit unbinds before disconnect().


⚠️ Low — extractEntityName uses brittle private-symbol access and can throw during render

File: src/utils/useMxObjectInfo.ts line 27
Note: Object.getOwnPropertySymbols(object)[0] reads an undocumented private Mendix internal symbol. If the platform changes the internal structure, this silently returns undefined and then throws "Unable to extract entity name" — inside a useMemo call, which propagates as an unhandled render error. Consider wrapping in a try/catch that returns undefined instead of throwing, and logging a clear warning. If the Mendix SDK eventually exposes entity name directly, the TODO comment in Pusher.tsx should be tracked in a follow-up ticket.


⚠️ Low — console.debug left in production event handler

File: src/Pusher.tsx line 18
Note: console.debug("[Pusher] Event received:", data) will log every received Pusher event in production. Remove or gate behind a dev-only flag.


Positives

  • useFetchPusherConfig correctly uses an active guard and AbortController together — both are needed and both are present.
  • PusherListener separates connection management from subscription management cleanly; destroy() / unsubscribe() boundary is well-defined.
  • CHANGELOG.md entry present for the initial release.
  • CSRF token is correctly forwarded both to the key-fetch REST call and to the Pusher auth endpoint.
  • useMxObjectInfo correctly guards against missing guid/entityName before constructing the subscription, preventing spurious subscriptions during load.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants