From 6302bb2c77c4a920e9563bd4095559acd742d80b Mon Sep 17 00:00:00 2001 From: Igor Kozlov Date: Thu, 27 Mar 2025 14:51:10 -0400 Subject: [PATCH] WIP --- examples/kitchen-sink/app/host.tsx | 108 ++++------------- examples/kitchen-sink/app/host/state.ts | 113 ------------------ examples/kitchen-sink/app/index.html | 17 --- .../app/remote/examples/App.svelte | 46 ------- .../kitchen-sink/app/remote/examples/App.vue | 51 -------- .../kitchen-sink/app/remote/examples/htm.ts | 58 --------- .../app/remote/examples/preact.tsx | 83 ------------- .../app/remote/examples/react-remote-ui.tsx | 88 ++++---------- .../app/remote/examples/react.tsx | 83 ------------- .../app/remote/examples/svelte.ts | 8 -- .../app/remote/examples/vanilla.ts | 83 ------------- .../kitchen-sink/app/remote/examples/vue.ts | 9 -- .../kitchen-sink/app/remote/iframe/index.html | 14 --- .../kitchen-sink/app/remote/iframe/sandbox.ts | 39 ------ examples/kitchen-sink/app/remote/render.ts | 34 +----- .../kitchen-sink/app/remote/worker/sandbox.ts | 35 ++---- examples/kitchen-sink/package.json | 3 +- packages/compat/source/adapter/host.ts | 60 +++++++--- pnpm-lock.yaml | 3 + 19 files changed, 102 insertions(+), 833 deletions(-) delete mode 100644 examples/kitchen-sink/app/host/state.ts delete mode 100644 examples/kitchen-sink/app/remote/examples/App.svelte delete mode 100644 examples/kitchen-sink/app/remote/examples/App.vue delete mode 100644 examples/kitchen-sink/app/remote/examples/htm.ts delete mode 100644 examples/kitchen-sink/app/remote/examples/preact.tsx delete mode 100644 examples/kitchen-sink/app/remote/examples/react.tsx delete mode 100644 examples/kitchen-sink/app/remote/examples/svelte.ts delete mode 100644 examples/kitchen-sink/app/remote/examples/vanilla.ts delete mode 100644 examples/kitchen-sink/app/remote/examples/vue.ts delete mode 100644 examples/kitchen-sink/app/remote/iframe/index.html delete mode 100644 examples/kitchen-sink/app/remote/iframe/sandbox.ts diff --git a/examples/kitchen-sink/app/host.tsx b/examples/kitchen-sink/app/host.tsx index eb9daf5d..923dc62a 100644 --- a/examples/kitchen-sink/app/host.tsx +++ b/examples/kitchen-sink/app/host.tsx @@ -1,128 +1,60 @@ +import '@preact/signals'; import {render} from 'preact'; import { RemoteRootRenderer, RemoteFragmentRenderer, createRemoteComponentRenderer, + SignalRemoteReceiver, } from '@remote-dom/preact/host'; -import {ThreadIframe, ThreadWebWorker} from '@quilted/threads'; -import type {SandboxAPI} from './types.ts'; -import {Button, Modal, Stack, Text, ControlPanel} from './host/components.tsx'; -import {createState} from './host/state.ts'; +import {createEndpoint, fromWebWorker, retain, release} from '@remote-ui/rpc'; + +import {Button, Stack, Text} from './host/components.tsx'; import {adaptToLegacyRemoteChannel} from '@remote-dom/compat'; -// We will put any remote elements we want to render in this root element. const uiRoot = document.querySelector('main')!; -// We use the `@quilted/threads` library to create a “thread” for our iframe, -// which lets us communicate over `postMessage` without having to worry about -// most of its complexities. -const iframe = document.querySelector('iframe')!; -const iframeSandbox = new ThreadIframe(iframe); - -// We also use the `@quilted/threads` library to create a “thread” around a Web -// Worker. We’ll run the same example code in both, depending on the `sandbox` -// state chosen by the user. const worker = new Worker( new URL('./remote/worker/sandbox.ts', import.meta.url), { type: 'module', }, ); -const workerSandbox = new ThreadWebWorker(worker); -// We will use Preact to render remote elements in this example. The Preact -// helper library lets you do this by mapping the name of a remote element to -// a local Preact component. We’ve implemented the actual UI of our components in -// the `./host/components.tsx` file, but we need to wrap each one in the `createRemoteComponentRenderer()` -// helper function in order to get some Preact niceties, like automatic conversion -// of slots to element props, and using the instance of a Preact component as the -// target for methods called on matching remote elements. +interface RemoteAPI { + renderLegacy: (channel: any) => Promise; +} + +const endpoint = createEndpoint(fromWebWorker(worker)); + const components = new Map([ ['ui-text', createRemoteComponentRenderer(Text)], ['ui-button', createRemoteComponentRenderer(Button)], ['ui-stack', createRemoteComponentRenderer(Stack)], - ['ui-modal', createRemoteComponentRenderer(Modal)], - // The `remote-fragment` element is a special element created by Remote DOM when - // it needs an unstyled container for a list of elements. This is primarily used - // to convert elements passed as a prop to React or Preact components into a slotted - // element. The `RemoteFragmentRenderer` component is provided to render this element - // on the host. ['remote-fragment', RemoteFragmentRenderer], ]); -// We offload most of the complex state logic to this `createState()` function. We’re -// just leaving the key bit in this file: when the example or sandbox changes, we render -// the example in the chosen sandbox. The `createState()` passes us a fresh `receiver` -// each time. This object, a `SignalRemoteReceiver`, keeps track of the tree of elements -// rendered by the remote environment. We use this object later to render these trees -// to Preact components using the `RemoteRootRenderer` component. - -const {receiver, example, sandbox} = createState( - async ({receiver, example, sandbox}) => { - const api = { - sandbox, - example, - async alert(content: string) { - console.log( - `Alert API used by example ${example} in the iframe sandbox`, - ); - window.alert(content); - }, - async closeModal() { - document.querySelector('dialog')?.close(); - }, - }; - - const sandboxToUse = sandbox === 'iframe' ? iframeSandbox : workerSandbox; - - if (example === 'react-remote-ui') { - const remoteUiChannel = adaptToLegacyRemoteChannel(receiver.connection, { - elements: { - Text: 'ui-text', - Button: 'ui-button', - Stack: 'ui-stack', - Modal: 'ui-modal', - }, - }); - await sandboxToUse.imports.renderLegacy(remoteUiChannel, { - ...api, - }); - } else { - await sandboxToUse.imports.render(receiver.connection, { - ...api, - }); - } +const receiver = new SignalRemoteReceiver({retain, release}); +const channel = adaptToLegacyRemoteChannel(receiver.connection, { + elements: { + Text: 'ui-text', + Button: 'ui-button', + Stack: 'ui-stack', }, -); +}); +endpoint.call.renderLegacy(channel); -// We render our Preact application, including the part that renders any remote -// elements for the current example, and the control panel that lets us change -// the framework or JavaScript sandbox being used. render( <> - , uiRoot, ); function ExampleRenderer() { - const value = receiver.value; - - if (value == null) return
Loading...
; - - if ('then' in value) { - return
Rendering example...
; - } - - if (value instanceof Error) { - return
Error while rendering example: {value.message}
; - } - return (
- +
); } diff --git a/examples/kitchen-sink/app/host/state.ts b/examples/kitchen-sink/app/host/state.ts deleted file mode 100644 index 2d4bae57..00000000 --- a/examples/kitchen-sink/app/host/state.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {signal, effect} from '@preact/signals'; -import {SignalRemoteReceiver} from '@remote-dom/preact/host'; - -import type {RenderExample, RenderSandbox} from '../types.ts'; - -const DEFAULT_SANDBOX = 'worker'; -const ALLOWED_SANDBOX_VALUES = new Set(['iframe', 'worker']); - -const DEFAULT_EXAMPLE = 'vanilla'; -const ALLOWED_EXAMPLE_VALUES = new Set([ - 'vanilla', - 'htm', - 'preact', - 'react', - 'svelte', - 'vue', -]); - -export function createState( - render: (details: { - example: RenderExample; - sandbox: RenderSandbox; - receiver: SignalRemoteReceiver; - }) => void | Promise, -) { - const initialURL = new URL(window.location.href); - - const sandboxQueryParam = initialURL.searchParams - .get('sandbox') - ?.toLowerCase() as RenderSandbox | undefined; - - const sandbox = signal( - ALLOWED_SANDBOX_VALUES.has(sandboxQueryParam!) - ? sandboxQueryParam! - : DEFAULT_SANDBOX, - ); - - const exampleQueryParam = initialURL.searchParams - .get('example') - ?.toLowerCase() as RenderExample | undefined; - - const example = signal( - ALLOWED_EXAMPLE_VALUES.has(exampleQueryParam!) - ? exampleQueryParam! - : DEFAULT_EXAMPLE, - ); - - const receiver = signal< - SignalRemoteReceiver | Error | Promise | undefined - >(undefined); - - const exampleCache = new Map< - string, - SignalRemoteReceiver | Error | Promise - >(); - - effect(() => { - const sandboxValue = sandbox.value; - const exampleValue = example.value; - - const key = `${sandboxValue}:${exampleValue}`; - let cached = exampleCache.get(key); - - const newURL = new URL(window.location.href); - - if (sandboxValue === DEFAULT_SANDBOX && exampleValue === DEFAULT_EXAMPLE) { - newURL.searchParams.delete('sandbox'); - newURL.searchParams.delete('example'); - } else { - newURL.searchParams.set('sandbox', sandboxValue); - newURL.searchParams.set('example', exampleValue); - } - - window.history.replaceState({}, '', newURL.toString()); - - if (cached == null) { - const receiver = new SignalRemoteReceiver(); - cached = Promise.resolve( - render({ - receiver, - sandbox: sandboxValue, - example: exampleValue, - }), - ) - .then(() => { - updateValueAfterRender(receiver); - return receiver; - }) - .catch((error) => { - updateValueAfterRender(error); - return Promise.reject(error); - }); - - exampleCache.set(key, cached); - } - - receiver.value = cached; - - function updateValueAfterRender(value: SignalRemoteReceiver | Error) { - exampleCache.set(key, value); - - if (sandboxValue !== sandbox.peek() || exampleValue !== example.peek()) { - return value; - } - - receiver.value = value; - - return value; - } - }); - - return {receiver, sandbox, example}; -} diff --git a/examples/kitchen-sink/app/index.html b/examples/kitchen-sink/app/index.html index 50afeea3..a56db746 100644 --- a/examples/kitchen-sink/app/index.html +++ b/examples/kitchen-sink/app/index.html @@ -10,23 +10,6 @@
- - - - diff --git a/examples/kitchen-sink/app/remote/examples/App.svelte b/examples/kitchen-sink/app/remote/examples/App.svelte deleted file mode 100644 index 8b439f8d..00000000 --- a/examples/kitchen-sink/app/remote/examples/App.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - - - - Rendering example: {api.example} - - - Rendering in sandbox: {api.sandbox} - - - - Open modal - - - Click count: {count} - Click me! - - Close - - - - diff --git a/examples/kitchen-sink/app/remote/examples/App.vue b/examples/kitchen-sink/app/remote/examples/App.vue deleted file mode 100644 index 6367896f..00000000 --- a/examples/kitchen-sink/app/remote/examples/App.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - diff --git a/examples/kitchen-sink/app/remote/examples/htm.ts b/examples/kitchen-sink/app/remote/examples/htm.ts deleted file mode 100644 index 65cbcb8d..00000000 --- a/examples/kitchen-sink/app/remote/examples/htm.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {html} from '@remote-dom/core/html'; - -import type {RenderAPI} from '../../types.ts'; -import type {Modal, Text} from '../elements.ts'; - -export function renderUsingHTM(root: Element, api: RenderAPI) { - let count = 0; - - function handlePress() { - updateCount(count + 1); - } - - function handleClose() { - if (count > 0) { - api.alert(`You clicked ${count} times!`); - } - - updateCount(0); - } - - function updateCount(newCount: number) { - count = newCount; - countText.textContent = String(count); - } - - function handlePrimaryAction() { - modal.close(); - } - - const countText = html` - ${count} - ` satisfies InstanceType; - - const modal = html` - - Click count: ${countText} - Click me! - - Close - - - ` satisfies InstanceType; - - const stack = html` - - - Rendering example: ${api.example} - - - Rendering in sandbox: ${api.sandbox} - - - Open modal ${modal} - - ` satisfies HTMLElement; - - root.append(stack); -} diff --git a/examples/kitchen-sink/app/remote/examples/preact.tsx b/examples/kitchen-sink/app/remote/examples/preact.tsx deleted file mode 100644 index d1ab89ec..00000000 --- a/examples/kitchen-sink/app/remote/examples/preact.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** @jsxRuntime automatic */ -/** @jsxImportSource preact */ - -import {render} from 'preact'; -import {useRef} from 'preact/hooks'; -import {useSignal} from '@preact/signals'; -import {createRemoteComponent} from '@remote-dom/preact'; - -import type {RenderAPI} from '../../types.ts'; -import { - Text as TextElement, - Button as ButtonElement, - Stack as StackElement, - Modal as ModalElement, -} from '../elements.ts'; - -const Text = createRemoteComponent('ui-text', TextElement); -const Button = createRemoteComponent('ui-button', ButtonElement, { - eventProps: { - onPress: {event: 'press'}, - }, -}); -const Stack = createRemoteComponent('ui-stack', StackElement); -const Modal = createRemoteComponent('ui-modal', ModalElement, { - eventProps: { - onOpen: {event: 'open'}, - onClose: {event: 'close'}, - }, -}); - -export function renderUsingPreact(root: Element, api: RenderAPI) { - render(, root); -} - -function App({api}: {api: RenderAPI}) { - return ( - - - Rendering example: {api.example} - - - Rendering in sandbox: {api.sandbox} - - - - ); -} - -function CountModal({alert}: Pick) { - const count = useSignal(0); - const modalRef = useRef>(null); - - const primaryAction = ( - - ); - - return ( - { - if (count.peek() > 0) { - alert(`You clicked ${count} times!`); - } - - count.value = 0; - }} - > - - - Click count: {count} - - - - - ); -} diff --git a/examples/kitchen-sink/app/remote/examples/react-remote-ui.tsx b/examples/kitchen-sink/app/remote/examples/react-remote-ui.tsx index 554fef89..c8451e7b 100644 --- a/examples/kitchen-sink/app/remote/examples/react-remote-ui.tsx +++ b/examples/kitchen-sink/app/remote/examples/react-remote-ui.tsx @@ -1,16 +1,10 @@ /** @jsxRuntime automatic */ /** @jsxImportSource react */ -import {retain} from '@quilted/threads'; +import {retain} from '@remote-ui/rpc'; import {createRemoteReactComponent} from '@remote-ui/react'; -import { - ButtonProperties, - ModalProperties, - RenderAPI, - StackProperties, - TextProperties, -} from '../../types'; -import {useState} from 'react'; +import {ButtonProperties, StackProperties} from '../../types'; +import {useCallback, useState} from 'react'; import {createRoot, createRemoteRoot} from '@remote-ui/react'; import {RemoteChannel} from '@remote-ui/core'; @@ -19,78 +13,44 @@ const Button = createRemoteReactComponent< ButtonProperties & {modal?: React.ReactNode} >('Button', {fragmentProps: ['modal']}); -const Text = createRemoteReactComponent<'Text', TextProperties>('Text'); const Stack = createRemoteReactComponent<'Stack', StackProperties>('Stack'); -const Modal = createRemoteReactComponent< - 'Modal', - ModalProperties & {primaryAction?: React.ReactNode} ->('Modal', {fragmentProps: ['primaryAction']}); -export function renderUsingReactRemoteUI( - channel: RemoteChannel, - api: RenderAPI, -) { - retain(api); +export function renderUsingReactRemoteUI(channel: RemoteChannel) { retain(channel); const remoteRoot = createRemoteRoot(channel, { components: ['Button', 'Text', 'Stack', 'Modal'], }); - createRoot(remoteRoot).render(); + createRoot(remoteRoot).render(); remoteRoot.mount(); } -function App({api}: {api: RenderAPI}) { - return ( - - - Rendering example: {api.example} - - - Rendering in sandbox: {api.sandbox} - - - - ); -} +function App() { + const [buttons, setButtons] = useState([ + {id: '1', label: 'Button 1'}, + {id: '2', label: 'Button 2'}, + ]); -function CountModal({alert, closeModal}: RenderAPI) { - const [count, setCount] = useState(0); - - const primaryAction = ( - - ); + const handleOrderChange = useCallback(() => { + setButtons([ + {id: '2', label: 'Button 2'}, + {id: '1', label: 'Button 1'}, + ]); + }, []); return ( - { - if (count > 0) { - alert(`You clicked ${count} times!`); - } + + - setCount(0); - }} - > - - - Click count: {count} - + {buttons.map((button) => ( - - + ))} + ); } diff --git a/examples/kitchen-sink/app/remote/examples/react.tsx b/examples/kitchen-sink/app/remote/examples/react.tsx deleted file mode 100644 index b3d37f7e..00000000 --- a/examples/kitchen-sink/app/remote/examples/react.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** @jsxRuntime automatic */ -/** @jsxImportSource react */ - -import {useState, useRef} from 'react'; -import {createRoot} from 'react-dom/client'; -import {createRemoteComponent} from '@remote-dom/react'; - -import type {RenderAPI} from '../../types.ts'; -import { - Text as TextElement, - Button as ButtonElement, - Stack as StackElement, - Modal as ModalElement, -} from '../elements.ts'; - -const Text = createRemoteComponent('ui-text', TextElement); -const Button = createRemoteComponent('ui-button', ButtonElement, { - eventProps: { - onPress: {event: 'press'}, - }, -}); - -const Stack = createRemoteComponent('ui-stack', StackElement); -const Modal = createRemoteComponent('ui-modal', ModalElement, { - eventProps: { - onOpen: {event: 'open'}, - onClose: {event: 'close'}, - }, -}); - -export function renderUsingReact(root: Element, api: RenderAPI) { - createRoot(root).render(); -} - -function App({api}: {api: RenderAPI}) { - return ( - - - Rendering example: {api.example} - - - Rendering in sandbox: {api.sandbox} - - - - ); -} - -function CountModal({alert}: Pick) { - const [count, setCount] = useState(0); - const modalRef = useRef>(null); - - const primaryAction = ( - - ); - - return ( - { - if (count > 0) { - alert(`You clicked ${count} times!`); - } - - setCount(0); - }} - > - - - Click count: {count} - - - - - ); -} diff --git a/examples/kitchen-sink/app/remote/examples/svelte.ts b/examples/kitchen-sink/app/remote/examples/svelte.ts deleted file mode 100644 index 924f9e23..00000000 --- a/examples/kitchen-sink/app/remote/examples/svelte.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type {RenderAPI} from '../../types.ts'; - -// @ts-ignore Not bothering to set up proper Svelte type-checking -import App from './App.svelte'; - -export function renderUsingSvelte(root: Element, api: RenderAPI) { - new App({target: root, props: {api}}); -} diff --git a/examples/kitchen-sink/app/remote/examples/vanilla.ts b/examples/kitchen-sink/app/remote/examples/vanilla.ts deleted file mode 100644 index f3c4ebf4..00000000 --- a/examples/kitchen-sink/app/remote/examples/vanilla.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type {RenderAPI} from '../../types.ts'; -import {Modal} from '../elements.ts'; - -export function renderUsingVanillaDOM(root: Element, api: RenderAPI) { - let count = 0; - - function handlePress() { - updateCount(count + 1); - } - - function handleClose() { - if (count > 0) { - api.alert(`You clicked ${count} times!`); - } - - updateCount(0); - } - - function updateCount(newCount: number) { - count = newCount; - countText.textContent = String(count); - } - - function handlePrimaryAction() { - modal.close(); - } - - const countText = document.createElement('ui-text'); - countText.textContent = String(count); - countText.setAttribute('emphasis', ''); - - const template = document.createElement('div'); - - template.innerHTML = ` - - Click count: - Click me! - - Close - - - `.trim(); - - const modal = template.querySelector('ui-modal')! as InstanceType< - typeof Modal - >; - - modal.addEventListener('close', handleClose); - - modal.querySelector('ui-text')!.append(countText); - - const [countButton, primaryActionButton] = [ - ...modal.querySelectorAll('ui-button'), - ]; - - countButton!.addEventListener('press', handlePress); - primaryActionButton!.addEventListener('press', handlePrimaryAction); - - template.innerHTML = ` - - - Rendering example: - - - Rendering in sandbox: - - - Open modal - - `.trim(); - - const stack = template.firstElementChild!; - - const [exampleText, sandboxText] = [ - ...stack.querySelectorAll('ui-text[emphasis]')!, - ]; - exampleText!.textContent = api.example; - sandboxText!.textContent = api.sandbox; - - stack.querySelector('ui-button')!.append(modal); - - root.append(stack); -} diff --git a/examples/kitchen-sink/app/remote/examples/vue.ts b/examples/kitchen-sink/app/remote/examples/vue.ts deleted file mode 100644 index 9b96dc4b..00000000 --- a/examples/kitchen-sink/app/remote/examples/vue.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {createApp} from 'vue'; -import type {RenderAPI} from '../../types.ts'; - -// @ts-ignore Not bothering to set up proper Vue type-checking -import App from './App.vue'; - -export function renderUsingVue(root: Element, api: RenderAPI) { - createApp(App, {api}).mount(root); -} diff --git a/examples/kitchen-sink/app/remote/iframe/index.html b/examples/kitchen-sink/app/remote/iframe/index.html deleted file mode 100644 index f6023b7f..00000000 --- a/examples/kitchen-sink/app/remote/iframe/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Kitchen sink (remote) • Remote DOM - - - - - - diff --git a/examples/kitchen-sink/app/remote/iframe/sandbox.ts b/examples/kitchen-sink/app/remote/iframe/sandbox.ts deleted file mode 100644 index c8792e7e..00000000 --- a/examples/kitchen-sink/app/remote/iframe/sandbox.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {RemoteMutationObserver} from '@remote-dom/core/elements'; -import {ThreadNestedIframe} from '@quilted/threads'; - -import '../elements.ts'; -import {render, renderLegacy} from '../render.ts'; -import type {SandboxAPI} from '../../types.ts'; - -// We use the `@quilted/threads` library to create a “thread” for our iframe, -// which lets us communicate over `postMessage` without having to worry about -// most of its complexities. -// -// This block exposes the `render` method that was used by the host application, -// in `index.html`. We receive the `RemoteConnection` object, and start synchronizing -// changes to the `
` element that contains our UI. -new ThreadNestedIframe({ - exports: { - async render(connection, api) { - // We will observe this DOM node, and send any elements within it to be - // reflected on this "host" page. - const root = document.createElement('div'); - root.id = `Example${api.example[0]!.toUpperCase()}${api.example.slice( - 1, - )}${api.sandbox[0]!.toUpperCase()}${api.sandbox.slice(1)}`; - - document.body.append(root); - - // We use the `RemoteMutationObserver` class, which extends the native DOM - // `MutationObserver`, to send any changes to a tree of DOM elements over - // a `RemoteConnection`. - const observer = new RemoteMutationObserver(connection); - observer.observe(root); - - await render(root, api); - }, - async renderLegacy(channel, api) { - await renderLegacy(channel, api); - }, - }, -}); diff --git a/examples/kitchen-sink/app/remote/render.ts b/examples/kitchen-sink/app/remote/render.ts index 42647656..4ce64deb 100644 --- a/examples/kitchen-sink/app/remote/render.ts +++ b/examples/kitchen-sink/app/remote/render.ts @@ -1,41 +1,11 @@ import {RemoteChannel} from '@remote-ui/core'; -import type {RenderAPI} from '../types.ts'; // Defines the custom elements available to render in the remote environment. import './elements.ts'; -export async function render(root: Element, api: RenderAPI) { - switch (api.example) { - case 'vanilla': { - const {renderUsingVanillaDOM} = await import('./examples/vanilla.ts'); - return renderUsingVanillaDOM(root, api); - } - case 'htm': { - const {renderUsingHTM} = await import('./examples/htm.ts'); - return renderUsingHTM(root, api); - } - case 'preact': { - const {renderUsingPreact} = await import('./examples/preact.tsx'); - return renderUsingPreact(root, api); - } - case 'react': { - const {renderUsingReact} = await import('./examples/react.tsx'); - return renderUsingReact(root, api); - } - case 'svelte': { - const {renderUsingSvelte} = await import('./examples/svelte.ts'); - return renderUsingSvelte(root, api); - } - case 'vue': { - const {renderUsingVue} = await import('./examples/vue.ts'); - return renderUsingVue(root, api); - } - } -} - -export async function renderLegacy(channel: RemoteChannel, api: RenderAPI) { +export async function renderLegacy(channel: RemoteChannel) { const {renderUsingReactRemoteUI} = await import( './examples/react-remote-ui.tsx' ); - return renderUsingReactRemoteUI(channel, api); + return renderUsingReactRemoteUI(channel); } diff --git a/examples/kitchen-sink/app/remote/worker/sandbox.ts b/examples/kitchen-sink/app/remote/worker/sandbox.ts index 90447cce..ef99ebd4 100644 --- a/examples/kitchen-sink/app/remote/worker/sandbox.ts +++ b/examples/kitchen-sink/app/remote/worker/sandbox.ts @@ -1,34 +1,15 @@ import '@remote-dom/core/polyfill'; import '@remote-dom/react/polyfill'; -import {ThreadWebWorker} from '@quilted/threads'; +import {createEndpoint} from '@remote-ui/rpc'; import '../elements.ts'; -import {render, renderLegacy} from '../render.ts'; -import type {SandboxAPI} from '../../types.ts'; +import {renderLegacy as renderLegacyRemote} from '../render.ts'; -// We use the `@quilted/threads` library to create a “thread” for our iframe, -// which lets us communicate over `postMessage` without having to worry about -// most of its complexities. -// -// This block exposes the `render` method that was used by the host application, -// in `index.html`. We receive the `RemoteConnection` object, and start synchronizing -// changes to a `` element that contains our UI. -new ThreadWebWorker(self as any as Worker, { - exports: { - async render(connection, api) { - // We will observe this DOM node, and send any elements within it to be - // reflected on this "host" page. This element is defined by the Remote DOM - // library, and provides a convenient `connect()` method that starts - // synchronizing its children over a `RemoteConnection`. - const root = document.createElement('remote-root'); - root.connect(connection); - document.body.append(root); +const endpoint = createEndpoint(self as any as Worker); - await render(root, api); - }, - async renderLegacy(channel, api) { - await renderLegacy(channel, api); - }, - }, -}); +endpoint.expose({renderLegacy}); + +async function renderLegacy(channel: any) { + await renderLegacyRemote(channel); +} diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json index 4385a09f..59f39f47 100644 --- a/examples/kitchen-sink/package.json +++ b/examples/kitchen-sink/package.json @@ -16,6 +16,7 @@ "@remote-dom/signals": "workspace:*", "@remote-ui/core": "^2.2.4", "@remote-ui/react": "^5.0.4", + "@remote-ui/rpc": "^1.4.5", "preact": "^10.22.0", "react": "^18.3.0", "react-dom": "^18.3.0", @@ -24,11 +25,11 @@ }, "devDependencies": { "@preact/preset-vite": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^3.1.2", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-vue": "^5.1.3", - "@sveltejs/vite-plugin-svelte": "^3.1.2", "vite": "^5.4.0" }, "sideEffects": [ diff --git a/packages/compat/source/adapter/host.ts b/packages/compat/source/adapter/host.ts index ac91637d..d4361e28 100644 --- a/packages/compat/source/adapter/host.ts +++ b/packages/compat/source/adapter/host.ts @@ -168,26 +168,52 @@ export function adaptToLegacyRemoteChannel( const parentNode = tree.get(parentId); - if (parentNode) { - const existingChildIndex = parentNode.findIndex( - ({id}) => id === child.id, - ); + // switch to false to fix the memory management issue + const BROKEN_MEMORY_MANAGEMENT = true; - if (existingChildIndex >= 0) { - records.push([ - MUTATION_TYPE_REMOVE_CHILD, - parentId, - existingChildIndex, - ] satisfies RemoteMutationRecord); + if (BROKEN_MEMORY_MANAGEMENT) { + if (parentNode) { + const existingChildIndex = parentNode.findIndex( + ({id}) => id === child.id, + ); + + if (existingChildIndex >= 0) { + records.push([ + MUTATION_TYPE_REMOVE_CHILD, + parentId, + existingChildIndex, + ] satisfies RemoteMutationRecord); + } } - } - records.push([ - MUTATION_TYPE_INSERT_CHILD, - parentId, - adaptLegacyNodeSerialization(child, options), - index, - ] satisfies RemoteMutationRecord); + records.push([ + MUTATION_TYPE_INSERT_CHILD, + parentId, + adaptLegacyNodeSerialization(child, options), + index, + ] satisfies RemoteMutationRecord); + } else { + records.push([ + MUTATION_TYPE_INSERT_CHILD, + parentId, + adaptLegacyNodeSerialization(child, options), + index + 1, + ] satisfies RemoteMutationRecord); + + if (parentNode) { + const existingChildIndex = parentNode.findIndex( + ({id}) => id === child.id, + ); + + if (existingChildIndex >= 0) { + records.push([ + MUTATION_TYPE_REMOVE_CHILD, + parentId, + existingChildIndex, + ] satisfies RemoteMutationRecord); + } + } + } mutate(records); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b090f807..2c782bd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: '@remote-ui/react': specifier: ^5.0.4 version: 5.0.4(react-reconciler@0.28.0(react@18.3.1))(react@18.3.1) + '@remote-ui/rpc': + specifier: ^1.4.5 + version: 1.4.5 preact: specifier: ^10.22.0 version: 10.22.1