[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<SerializedEventData> instead of bare RemoteEvent, giving front-component authors typed access to event details
This commit is contained in:
@@ -20,6 +20,7 @@ var TestComponent = () => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var test_component_front_component_default = globalThis.jsx(TestComponent, {});
|
||||
export {
|
||||
test_component_front_component_default as default
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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<string, unknown> | 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<string, unknown>) => {
|
||||
} 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) {
|
||||
|
||||
@@ -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<SerializedEventData>): 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<SerializedEventData>): 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,
|
||||
|
||||
@@ -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<string, string> = {
|
||||
@@ -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<string, unknown>;
|
||||
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<string, unknown> | 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<string, unknown>) => {
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -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<SerializedEventData>): void;
|
||||
dblclick(event: RemoteEvent<SerializedEventData>): void;
|
||||
mousedown(event: RemoteEvent<SerializedEventData>): void;
|
||||
mouseup(event: RemoteEvent<SerializedEventData>): void;
|
||||
mouseover(event: RemoteEvent<SerializedEventData>): void;
|
||||
mouseout(event: RemoteEvent<SerializedEventData>): void;
|
||||
mouseenter(event: RemoteEvent<SerializedEventData>): void;
|
||||
mouseleave(event: RemoteEvent<SerializedEventData>): void;
|
||||
keydown(event: RemoteEvent<SerializedEventData>): void;
|
||||
keyup(event: RemoteEvent<SerializedEventData>): void;
|
||||
keypress(event: RemoteEvent<SerializedEventData>): void;
|
||||
focus(event: RemoteEvent<SerializedEventData>): void;
|
||||
blur(event: RemoteEvent<SerializedEventData>): void;
|
||||
change(event: RemoteEvent<SerializedEventData>): void;
|
||||
input(event: RemoteEvent<SerializedEventData>): void;
|
||||
submit(event: RemoteEvent<SerializedEventData>): void;
|
||||
scroll(event: RemoteEvent<SerializedEventData>): void;
|
||||
wheel(event: RemoteEvent<SerializedEventData>): void;
|
||||
contextmenu(event: RemoteEvent<SerializedEventData>): void;
|
||||
drag(event: RemoteEvent<SerializedEventData>): void;
|
||||
};
|
||||
|
||||
const HTML_COMMON_EVENTS_ARRAY = [
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user