diff --git a/productivity-suite/app/fragments/todos/src/components/Header.tsx b/productivity-suite/app/fragments/todos/src/components/Header.tsx index d4238657..4a2f1480 100644 --- a/productivity-suite/app/fragments/todos/src/components/Header.tsx +++ b/productivity-suite/app/fragments/todos/src/components/Header.tsx @@ -26,6 +26,9 @@ export function Header({ }`} placeholder="What needs to be done?" autoFocus + // Spellcheck set to false to work around a memory leak issue in chrome: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1410394 + spellCheck={false} value={newTodoDetails.text} onInput={(event: ChangeEvent) => { const text = event.target.value; diff --git a/productivity-suite/app/legacy-app/index.html b/productivity-suite/app/legacy-app/index.html index 8fdceb80..bbd96812 100644 --- a/productivity-suite/app/legacy-app/index.html +++ b/productivity-suite/app/legacy-app/index.html @@ -155,7 +155,7 @@ } body.show-seams #root > .layout, - body.show-seams piercing-fragment-host { + body.show-seams piercing-fragment-host:has(*) { outline: 2px dashed var(--seams-color); border-radius: 1px; } diff --git a/productivity-suite/piercing-library/src/message-bus/message-bus.ts b/productivity-suite/piercing-library/src/message-bus/message-bus.ts index 3de821dc..ae330eaa 100644 --- a/productivity-suite/piercing-library/src/message-bus/message-bus.ts +++ b/productivity-suite/piercing-library/src/message-bus/message-bus.ts @@ -60,6 +60,11 @@ export class GenericMessageBus implements MessageBus { } dispatch(eventName: string, value: JSONValue) { + // If the `value` here is an object that originates in a iframe, then it + // will hold a reference to the iframe Window through the `Object`'s + // prototype chain, causing a leak. Cloning it here avoids this + value = structuredClone(value); + this._state[eventName] = value; const callbacksForEvent = this._callbacksMap.get(eventName) ?? []; setTimeout( diff --git a/productivity-suite/piercing-library/src/piercing-fragment-outlet.ts b/productivity-suite/piercing-library/src/piercing-fragment-outlet.ts index 94fc9785..ad55f1cc 100644 --- a/productivity-suite/piercing-library/src/piercing-fragment-outlet.ts +++ b/productivity-suite/piercing-library/src/piercing-fragment-outlet.ts @@ -80,7 +80,32 @@ export class PiercingFragmentOutlet extends HTMLElement { disconnectedCallback() { if (this.fragmentHost) { - unmountedFragmentIds.add(this.fragmentHost.fragmentId); + const fragmentId = this.fragmentHost.fragmentId; + unmountedFragmentIds.add(fragmentId); + + // Loop through listeners created by the fragment and clean them up. + // fragmentRegistrationMap is initialized by `reframed-host` and then + // registered in reframed-client. + const fragmentFunctionConstructor = + fragmentRegistrationMap.get(fragmentId)!; + + if (fragmentFunctionConstructor) { + fragmentRegistrationMap.delete(fragmentId); + + const listenerRegistrations = fragmentListenerMap.get( + fragmentFunctionConstructor + )!; + fragmentListenerMap.delete(fragmentFunctionConstructor); + + for (const listenerRegistration of listenerRegistrations) { + listenerRegistration.target.removeEventListener( + listenerRegistration.name, + listenerRegistration.listener, + listenerRegistration.options + ); + } + } + this.fragmentHost = null; } } diff --git a/productivity-suite/piercing-library/src/piercing-gateway.ts b/productivity-suite/piercing-library/src/piercing-gateway.ts index 72b9f307..721fe423 100644 --- a/productivity-suite/piercing-library/src/piercing-gateway.ts +++ b/productivity-suite/piercing-library/src/piercing-gateway.ts @@ -10,6 +10,7 @@ import { // for debugging replace `qwikloader.js` with `qwikloader.debug.js` to have the code non-minified import qwikloader from "@builder.io/qwik/qwikloader.js?raw"; import reframedClient from "./reframed-client?raw"; +import reframedHost from "./reframed-host?raw"; import { MessageBusState } from "./message-bus/message-bus"; import { getMessageBusState } from "./message-bus/server-side-message-bus"; @@ -321,23 +322,19 @@ export class PiercingGateway { const requestIsForHtml = request.headers .get("Accept") ?.includes("text/html"); + if (requestIsForHtml) { const stateHeaderStr = request.headers.get("message-bus-state"); - let indexBody = (await response.text()).replace( - "", - `${getMessageBusInlineScript(stateHeaderStr ?? "{}")}\n` + - `${piercingFragmentHostInlineScript}\n` + - "" - ); + const isolateFragments = this.config.isolateFragments?.(env); + + const postHead = ` + ${getMessageBusInlineScript(stateHeaderStr ?? "{}")} + ${piercingFragmentHostInlineScript} + ${isolateFragments ? getReframedHostCode() : qwikloaderScript} + + `; + let indexBody = (await response.text()).replace("", postHead); - if (!this.config.isolateFragments?.(env)) { - // We need to include the qwikLoader script here - // this is a temporary bugfix, see: https://jira.cfops.it/browse/DEVDASH-51 - indexBody = indexBody.replace( - "", - `\n${qwikloaderScript}` - ); - } return new Response(indexBody, response); } @@ -372,12 +369,13 @@ export class PiercingGateway { if (this.config.isolateFragments?.(env)) { // Quotes must be escaped from srcdoc contents template = ` + ${prePiercingStyles} - ${prePiercingStyles}