From c6d04ccced467dcd4e1de7426e5d616194301322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:22:54 +0100 Subject: [PATCH] [FRONT COMPONENTS] Serialize events through the worker boundary (#17767) - Introduces a SerializedEventData type that captures only serializable properties from DOM/React events - Updates `wrapEventHandler` in the host component registry to serialize native events via serializeEvent() before passing them across the worker boundary via postMessage, avoiding circular references and non-cloneable DOM nodes - Updates generated remote element event signatures to use RemoteEvent instead of bare RemoteEvent, giving front-component authors typed access to event details --- .../utils/mockFrontComponent.ts | 1 + .../generators/host-registry.generator.ts | 65 +++++++++++++++- .../generators/remote-elements.generator.ts | 11 ++- .../host/generated/host-component-registry.ts | 75 +++++++++++++++++-- .../remote/generated/remote-elements.ts | 41 +++++----- .../SerializedEventData.ts | 26 +++++++ .../src/sdk/front-component-common/index.ts | 1 + 7 files changed, 189 insertions(+), 31 deletions(-) create mode 100644 packages/twenty-sdk/src/sdk/front-component-common/SerializedEventData.ts diff --git a/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts b/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts index 0441cca5ed..84386a5aa2 100644 --- a/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts +++ b/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts @@ -20,6 +20,7 @@ var TestComponent = () => { } ); }; + var test_component_front_component_default = globalThis.jsx(TestComponent, {}); export { test_component_front_component_default as default diff --git a/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts b/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts index 331d0947c4..fd1f105b6f 100644 --- a/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts +++ b/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts @@ -40,9 +40,61 @@ const parseStyle = (styleString: string | undefined): React.CSSProperties | unde return style; }; -const wrapEventHandler = (handler: () => void) => { - return (_event: unknown) => { - handler(); +const serializeEvent = (event: unknown): SerializedEventData => { + if (!event || typeof event !== 'object') { + return { type: 'unknown' }; + } + + const domEvent = event as Record; + const serialized: SerializedEventData = { + type: typeof domEvent.type === 'string' ? domEvent.type : 'unknown', + }; + + if ('altKey' in domEvent) serialized.altKey = domEvent.altKey as boolean; + if ('ctrlKey' in domEvent) serialized.ctrlKey = domEvent.ctrlKey as boolean; + if ('metaKey' in domEvent) serialized.metaKey = domEvent.metaKey as boolean; + if ('shiftKey' in domEvent) serialized.shiftKey = domEvent.shiftKey as boolean; + + if ('clientX' in domEvent) serialized.clientX = domEvent.clientX as number; + if ('clientY' in domEvent) serialized.clientY = domEvent.clientY as number; + if ('pageX' in domEvent) serialized.pageX = domEvent.pageX as number; + if ('pageY' in domEvent) serialized.pageY = domEvent.pageY as number; + if ('screenX' in domEvent) serialized.screenX = domEvent.screenX as number; + if ('screenY' in domEvent) serialized.screenY = domEvent.screenY as number; + if ('button' in domEvent) serialized.button = domEvent.button as number; + if ('buttons' in domEvent) serialized.buttons = domEvent.buttons as number; + + if ('key' in domEvent) serialized.key = domEvent.key as string; + if ('code' in domEvent) serialized.code = domEvent.code as string; + if ('repeat' in domEvent) serialized.repeat = domEvent.repeat as boolean; + + if ('deltaX' in domEvent) serialized.deltaX = domEvent.deltaX as number; + if ('deltaY' in domEvent) serialized.deltaY = domEvent.deltaY as number; + if ('deltaZ' in domEvent) serialized.deltaZ = domEvent.deltaZ as number; + if ('deltaMode' in domEvent) serialized.deltaMode = domEvent.deltaMode as number; + + const target = domEvent.target as Record | undefined; + if (target && typeof target === 'object') { + if ('value' in target && typeof target.value === 'string') { + serialized.value = target.value; + } + if ('checked' in target && typeof target.checked === 'boolean') { + serialized.checked = target.checked; + } + if ('scrollTop' in target && typeof target.scrollTop === 'number') { + serialized.scrollTop = target.scrollTop; + } + if ('scrollLeft' in target && typeof target.scrollLeft === 'number') { + serialized.scrollLeft = target.scrollLeft; + } + } + + return serialized; +}; + +const wrapEventHandler = (handler: (detail: SerializedEventData) => void) => { + return (event: unknown) => { + handler(serializeEvent(event)); }; }; @@ -56,7 +108,7 @@ const filterProps = (props: Record) => { } else { const normalizedKey = EVENT_NAME_MAP[key.toLowerCase()] || key; if (normalizedKey.startsWith('on') && typeof value === 'function') { - filtered[normalizedKey] = wrapEventHandler(value as () => void); + filtered[normalizedKey] = wrapEventHandler(value as (detail: SerializedEventData) => void); } else { filtered[normalizedKey] = value; } @@ -172,6 +224,11 @@ export const generateHostRegistry = ( namedImports: ['RemoteFragmentRenderer', 'createRemoteComponentRenderer'], }); + sourceFile.addImportDeclaration({ + moduleSpecifier: '../../../sdk/front-component-common/SerializedEventData', + namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }], + }); + const uiImports = groupImportsByPath(components); for (const [modulePath, namedImports] of uiImports) { diff --git a/packages/twenty-sdk/scripts/remote-dom/generators/remote-elements.generator.ts b/packages/twenty-sdk/scripts/remote-dom/generators/remote-elements.generator.ts index 5afa7e97b2..8e5f365607 100644 --- a/packages/twenty-sdk/scripts/remote-dom/generators/remote-elements.generator.ts +++ b/packages/twenty-sdk/scripts/remote-dom/generators/remote-elements.generator.ts @@ -84,7 +84,9 @@ const generateCommonEventsType = ( type: (writer) => { writer.block(() => { for (const event of events) { - writer.writeLine(`${event}(event: RemoteEvent): void;`); + writer.writeLine( + `${event}(event: RemoteEvent): void;`, + ); } }); }, @@ -184,7 +186,7 @@ const generateElementDefinition = ( const eventsType = hasEvents ? useSharedEvents ? TYPE_NAMES.COMMON_EVENTS - : `{ ${component.events.map((event) => `${event}(event: RemoteEvent): void`).join('; ')} }` + : `{ ${component.events.map((event) => `${event}(event: RemoteEvent): void`).join('; ')} }` : TYPE_NAMES.EMPTY_RECORD; sourceFile.addVariableStatement({ @@ -346,6 +348,11 @@ export const generateRemoteElements = ( ], }); + sourceFile.addImportDeclaration({ + moduleSpecifier: '../../../sdk/front-component-common/SerializedEventData', + namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }], + }); + const commonPropertyNames = new Set(Object.keys(commonProperties)); const componentsWithProps = prepareComponentsWithSpecificProps( components, diff --git a/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts b/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts index 5668035e9e..d1c64c2a3a 100644 --- a/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts +++ b/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts @@ -7,12 +7,13 @@ * |___/ */ -import React from 'react'; import { RemoteFragmentRenderer, createRemoteComponentRenderer, } from '@remote-dom/react/host'; +import React from 'react'; import { Button } from 'twenty-ui/input'; +import { type SerializedEventData } from '../../../sdk/front-component-common/SerializedEventData'; const INTERNAL_PROPS = new Set(['element', 'receiver', 'components']); const EVENT_NAME_MAP: Record = { @@ -64,9 +65,71 @@ const parseStyle = ( return style; }; -const wrapEventHandler = (handler: () => void) => { - return (_event: unknown) => { - handler(); +// Extracts only serializable properties from DOM/React events. +// Native events contain circular references and DOM nodes that cannot +// cross the worker boundary via postMessage. +const serializeEvent = (event: unknown): SerializedEventData => { + if (!event || typeof event !== 'object') { + return { type: 'unknown' }; + } + + const domEvent = event as Record; + const serialized: SerializedEventData = { + type: typeof domEvent.type === 'string' ? domEvent.type : 'unknown', + }; + + // Modifier keys (shared by mouse and keyboard events) + if ('altKey' in domEvent) serialized.altKey = domEvent.altKey as boolean; + if ('ctrlKey' in domEvent) serialized.ctrlKey = domEvent.ctrlKey as boolean; + if ('metaKey' in domEvent) serialized.metaKey = domEvent.metaKey as boolean; + if ('shiftKey' in domEvent) + serialized.shiftKey = domEvent.shiftKey as boolean; + + // Mouse event properties + if ('clientX' in domEvent) serialized.clientX = domEvent.clientX as number; + if ('clientY' in domEvent) serialized.clientY = domEvent.clientY as number; + if ('pageX' in domEvent) serialized.pageX = domEvent.pageX as number; + if ('pageY' in domEvent) serialized.pageY = domEvent.pageY as number; + if ('screenX' in domEvent) serialized.screenX = domEvent.screenX as number; + if ('screenY' in domEvent) serialized.screenY = domEvent.screenY as number; + if ('button' in domEvent) serialized.button = domEvent.button as number; + if ('buttons' in domEvent) serialized.buttons = domEvent.buttons as number; + + // Keyboard event properties + if ('key' in domEvent) serialized.key = domEvent.key as string; + if ('code' in domEvent) serialized.code = domEvent.code as string; + if ('repeat' in domEvent) serialized.repeat = domEvent.repeat as boolean; + + // Wheel event properties + if ('deltaX' in domEvent) serialized.deltaX = domEvent.deltaX as number; + if ('deltaY' in domEvent) serialized.deltaY = domEvent.deltaY as number; + if ('deltaZ' in domEvent) serialized.deltaZ = domEvent.deltaZ as number; + if ('deltaMode' in domEvent) + serialized.deltaMode = domEvent.deltaMode as number; + + // Extract value/checked from target (for input/change/select events) + const target = domEvent.target as Record | undefined; + if (target && typeof target === 'object') { + if ('value' in target && typeof target.value === 'string') { + serialized.value = target.value; + } + if ('checked' in target && typeof target.checked === 'boolean') { + serialized.checked = target.checked; + } + if ('scrollTop' in target && typeof target.scrollTop === 'number') { + serialized.scrollTop = target.scrollTop; + } + if ('scrollLeft' in target && typeof target.scrollLeft === 'number') { + serialized.scrollLeft = target.scrollLeft; + } + } + + return serialized; +}; + +const wrapEventHandler = (handler: (detail: SerializedEventData) => void) => { + return (event: unknown) => { + handler(serializeEvent(event)); }; }; @@ -80,7 +143,9 @@ const filterProps = (props: Record) => { } else { const normalizedKey = EVENT_NAME_MAP[key.toLowerCase()] || key; if (normalizedKey.startsWith('on') && typeof value === 'function') { - filtered[normalizedKey] = wrapEventHandler(value as () => void); + filtered[normalizedKey] = wrapEventHandler( + value as (detail: SerializedEventData) => void, + ); } else { filtered[normalizedKey] = value; } diff --git a/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts b/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts index 254606d588..8b89c2b702 100644 --- a/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts +++ b/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts @@ -13,6 +13,7 @@ import { RemoteFragmentElement, type RemoteEvent, } from '@remote-dom/core/elements'; +import { type SerializedEventData } from '../../../sdk/front-component-common/SerializedEventData'; export type HtmlCommonProperties = { id?: string; @@ -26,26 +27,26 @@ export type HtmlCommonProperties = { 'data-testid'?: string; }; export type HtmlCommonEvents = { - click(event: RemoteEvent): void; - dblclick(event: RemoteEvent): void; - mousedown(event: RemoteEvent): void; - mouseup(event: RemoteEvent): void; - mouseover(event: RemoteEvent): void; - mouseout(event: RemoteEvent): void; - mouseenter(event: RemoteEvent): void; - mouseleave(event: RemoteEvent): void; - keydown(event: RemoteEvent): void; - keyup(event: RemoteEvent): void; - keypress(event: RemoteEvent): void; - focus(event: RemoteEvent): void; - blur(event: RemoteEvent): void; - change(event: RemoteEvent): void; - input(event: RemoteEvent): void; - submit(event: RemoteEvent): void; - scroll(event: RemoteEvent): void; - wheel(event: RemoteEvent): void; - contextmenu(event: RemoteEvent): void; - drag(event: RemoteEvent): void; + click(event: RemoteEvent): void; + dblclick(event: RemoteEvent): void; + mousedown(event: RemoteEvent): void; + mouseup(event: RemoteEvent): void; + mouseover(event: RemoteEvent): void; + mouseout(event: RemoteEvent): void; + mouseenter(event: RemoteEvent): void; + mouseleave(event: RemoteEvent): void; + keydown(event: RemoteEvent): void; + keyup(event: RemoteEvent): void; + keypress(event: RemoteEvent): void; + focus(event: RemoteEvent): void; + blur(event: RemoteEvent): void; + change(event: RemoteEvent): void; + input(event: RemoteEvent): void; + submit(event: RemoteEvent): void; + scroll(event: RemoteEvent): void; + wheel(event: RemoteEvent): void; + contextmenu(event: RemoteEvent): void; + drag(event: RemoteEvent): void; }; const HTML_COMMON_EVENTS_ARRAY = [ diff --git a/packages/twenty-sdk/src/sdk/front-component-common/SerializedEventData.ts b/packages/twenty-sdk/src/sdk/front-component-common/SerializedEventData.ts new file mode 100644 index 0000000000..7671ed3a1b --- /dev/null +++ b/packages/twenty-sdk/src/sdk/front-component-common/SerializedEventData.ts @@ -0,0 +1,26 @@ +export type SerializedEventData = { + type: string; + altKey?: boolean; + ctrlKey?: boolean; + metaKey?: boolean; + shiftKey?: boolean; + clientX?: number; + clientY?: number; + pageX?: number; + pageY?: number; + screenX?: number; + screenY?: number; + button?: number; + buttons?: number; + key?: string; + code?: string; + repeat?: boolean; + value?: string; + checked?: boolean; + scrollTop?: number; + scrollLeft?: number; + deltaX?: number; + deltaY?: number; + deltaZ?: number; + deltaMode?: number; +}; diff --git a/packages/twenty-sdk/src/sdk/front-component-common/index.ts b/packages/twenty-sdk/src/sdk/front-component-common/index.ts index b5a290afe7..3bb42cfa91 100644 --- a/packages/twenty-sdk/src/sdk/front-component-common/index.ts +++ b/packages/twenty-sdk/src/sdk/front-component-common/index.ts @@ -15,3 +15,4 @@ export { COMMON_HTML_EVENTS } from './CommonHtmlEvents'; export { EVENT_TO_REACT } from './EventToReact'; export { HTML_COMMON_PROPERTIES } from './HtmlCommonProperties'; export { HTML_TAG_TO_REMOTE_COMPONENT } from './HtmlTagToRemoteComponent'; +export type { SerializedEventData } from './SerializedEventData';