Skip to content

Commit

Permalink
refactor(runtime): make WeakMap obsolete (#6156)
Browse files Browse the repository at this point in the history
* poc: use hostrefs inside nodes

* Add cleanup logic of vnode

* fix typings

* remove vdome cleanup to void stable refs

* remove dev log

* fix return types of delete

* use get function to resolve hostref

* remove not needed function

* change type of childNode

* remove WeapMap implementation from hydration and testing
  • Loading branch information
danielleroux authored Feb 13, 2025
1 parent 3b47de6 commit 4add75c
Show file tree
Hide file tree
Showing 9 changed files with 40 additions and 90 deletions.
43 changes: 12 additions & 31 deletions src/client/client-host-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,19 @@ import { reWireGetterSetter } from '@utils/es2022-rewire-class-members';

import type * as d from '../declarations';

/**
* A WeakMap mapping runtime component references to their corresponding host reference
* instances.
*
* **Note**: If we're in an HMR context we need to store a reference to this
* value on `window` in order to maintain the mapping of {@link d.RuntimeRef}
* to {@link d.HostRef} across HMR updates.
*
* This is necessary because when HMR updates for a component are processed by
* the browser-side dev server client the JS bundle for that component is
* re-fetched. Since the module containing {@link hostRefs} is included in
* that bundle, if we do not store a reference to it the new iteration of the
* component will not have access to the previous hostRef map, leading to a
* bug where the new version of the component cannot properly initialize.
*/
const hostRefs: WeakMap<d.RuntimeRef, d.HostRef> = /*@__PURE__*/ BUILD.hotModuleReplacement
? ((window as any).__STENCIL_HOSTREFS__ ||= new WeakMap())
: new WeakMap();

/**
* Given a {@link d.RuntimeRef} remove the corresponding {@link d.HostRef} from
* the {@link hostRefs} WeakMap.
*
* @param ref the runtime ref of interest
* @returns — true if the element was successfully removed, or false if it was not present.
*/
export const deleteHostRef = (ref: d.RuntimeRef) => hostRefs.delete(ref);

/**
* Given a {@link d.RuntimeRef} retrieve the corresponding {@link d.HostRef}
*
* @param ref the runtime ref of interest
* @returns the Host reference (if found) or undefined
*/
export const getHostRef = (ref: d.RuntimeRef): d.HostRef | undefined => hostRefs.get(ref);
export const getHostRef = (ref: d.RuntimeRef): d.HostRef | undefined => {
if (ref.__stencil__getHostRef) {
return ref.__stencil__getHostRef();
}

return undefined;
};

/**
* Register a lazy instance with the {@link hostRefs} object so it's
Expand All @@ -47,7 +25,9 @@ export const getHostRef = (ref: d.RuntimeRef): d.HostRef | undefined => hostRefs
* @param hostRef that instances `HostRef` object
*/
export const registerInstance = (lazyInstance: any, hostRef: d.HostRef) => {
hostRefs.set((hostRef.$lazyInstance$ = lazyInstance), hostRef);
lazyInstance.__stencil__getHostRef = () => hostRef;
hostRef.$lazyInstance$ = lazyInstance;

if (BUILD.modernPropertyDecls && (BUILD.state || BUILD.prop)) {
reWireGetterSetter(lazyInstance, hostRef);
}
Expand Down Expand Up @@ -81,7 +61,8 @@ export const registerHost = (hostElement: d.HostElement, cmpMeta: d.ComponentRun
hostElement['s-rc'] = [];
}

const ref = hostRefs.set(hostElement, hostRef);
const ref = hostRef;
hostElement.__stencil__getHostRef = () => ref;

if (!BUILD.lazyLoad && BUILD.modernPropertyDecls && (BUILD.state || BUILD.prop)) {
reWireGetterSetter(hostElement, hostRef);
Expand Down
4 changes: 3 additions & 1 deletion src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,8 @@ export interface HostElement extends HTMLElement {
host?: Element;
forceUpdate?: () => void;

__stencil__getHostRef?: () => HostRef;

// "s-" prefixed properties should not be property renamed
// and should be common between all versions of stencil

Expand Down Expand Up @@ -1724,7 +1726,7 @@ export type ComponentRuntimeReflectingAttr = [string, string | undefined];
* keys in a `WeakMap` which maps {@link HostElement} instances to their
* associated {@link HostRef} instance.
*/
export type RuntimeRef = HostElement | {};
export type RuntimeRef = HostElement | { __stencil__getHostRef?: () => HostRef };

/**
* Interface used to track an Element, it's virtual Node (`VNode`), and other data
Expand Down
19 changes: 13 additions & 6 deletions src/hydrate/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,22 @@ export const supportsListenerOptions = false;

export const supportsConstructableStylesheets = false;

const hostRefs: WeakMap<d.RuntimeRef, d.HostRef> = new WeakMap();
export const getHostRef = (ref: d.RuntimeRef) => {
if (ref.__stencil__getHostRef) {
return ref.__stencil__getHostRef();
}

export const getHostRef = (ref: d.RuntimeRef) => hostRefs.get(ref);
export const deleteHostRef = (ref: d.RuntimeRef) => hostRefs.delete(ref);
return undefined;
};

export const registerInstance = (lazyInstance: any, hostRef: d.HostRef) => {
const ref = hostRefs.set((hostRef.$lazyInstance$ = lazyInstance), hostRef);
lazyInstance.__stencil__getHostRef = () => hostRef;
hostRef.$lazyInstance$ = lazyInstance;

if (BUILD.modernPropertyDecls && (BUILD.state || BUILD.prop)) {
reWireGetterSetter(lazyInstance, hostRef);
}
return ref;
return hostRef;
};

export const registerHost = (elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta) => {
Expand All @@ -152,7 +157,9 @@ export const registerHost = (elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta
hostRef.$onReadyPromise$ = new Promise((r) => (hostRef.$onReadyResolve$ = r));
elm['s-p'] = [];
elm['s-rc'] = [];
return hostRefs.set(elm, hostRef);
elm.__stencil__getHostRef = () => hostRef;

return hostRef;
};

export const Build: d.UserBuildConditionals = {
Expand Down
25 changes: 1 addition & 24 deletions src/runtime/bootstrap-custom-element.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { BUILD } from '@app-data';
import {
addHostEventListeners,
deleteHostRef,
forceUpdate,
getHostRef,
plt,
registerHost,
styles,
supportsShadow,
} from '@platform';
import { addHostEventListeners, forceUpdate, getHostRef, registerHost, styles, supportsShadow } from '@platform';
import { CMP_FLAGS } from '@utils';

import type * as d from '../declarations';
Expand Down Expand Up @@ -102,20 +93,6 @@ export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMet
if (BUILD.disconnectedCallback && originalDisconnectedCallback) {
originalDisconnectedCallback.call(this);
}

/**
* Clean up Node references lingering around in `hostRef` objects
* to ensure GC can clean up the memory.
*/
plt.raf(() => {
const hostRef = getHostRef(this);
if (hostRef?.$vnode$?.$elm$ instanceof Node && !hostRef.$vnode$.$elm$.isConnected) {
delete hostRef.$vnode$;
}
if (this instanceof Node && !this.isConnected) {
deleteHostRef(this);
}
});
},
__attachShadow() {
if (supportsShadow) {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/vdom/vdom-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const parseVNodeAnnotations = (
*/
const childNodes = [...Array.from(node.childNodes), ...Array.from(node.shadowRoot?.childNodes || [])];
childNodes.forEach((childNode) => {
const hostRef = getHostRef(childNode);
const hostRef = getHostRef(childNode as d.RuntimeRef);
if (hostRef != null && !docData.staticComponents.has(childNode.nodeName.toLowerCase())) {
const cmpData: CmpData = {
nodeIds: 0,
Expand Down
2 changes: 1 addition & 1 deletion src/testing/platform/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { Build } from './testing-build';
export { modeResolutionChain, styles } from './testing-constants';
export { deleteHostRef, getHostRef, registerHost, registerInstance } from './testing-host-ref';
export { getHostRef, registerHost, registerInstance } from './testing-host-ref';
export { consoleDevError, consoleDevInfo, consoleDevWarn, consoleError, setErrorHandler } from './testing-log';
export {
isMemberInElement,
Expand Down
5 changes: 0 additions & 5 deletions src/testing/platform/testing-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,3 @@ export const queuedLoadModules: QueuedLoadModule[] = [];
* A collection of errors that were detected to surface during the rendering process
*/
export const caughtErrors: Error[] = [];
/**
* A mapping of runtime references to HTML elements to the data structure Stencil uses to track the element alongside
* additional metadata
*/
export const hostRefs = new Map<d.RuntimeRef, d.HostRef>();
27 changes: 8 additions & 19 deletions src/testing/platform/testing-host-ref.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
import type * as d from '@stencil/core/internal';

import { hostRefs } from './testing-constants';

/**
* Retrieve the data structure tracking the component by its runtime reference
* @param elm the reference to the element
* @returns the corresponding Stencil reference data structure, or undefined if one cannot be found
*/
export const getHostRef = (elm: d.RuntimeRef | undefined): d.HostRef | undefined => {
return hostRefs.get(elm);
};
if (elm.__stencil__getHostRef) {
return elm.__stencil__getHostRef();
}

/**
* Given a {@link d.RuntimeRef} remove the corresponding {@link d.HostRef} from
* the {@link hostRefs} WeakMap.
*
* @param ref the runtime ref of interest
* @returns — true if the element was successfully removed, or false if it was not present.
*/
export const deleteHostRef = (ref: d.RuntimeRef) => hostRefs.delete(ref);
return undefined;
};

/**
* Add the provided `hostRef` instance to the global {@link hostRefs} map, using the provided `lazyInstance` as a key.
* @param lazyInstance a Stencil component instance
* @param hostRef an optional reference to Stencil's tracking data for the component. If none is provided, one will be created.
* @returns the updated `hostRefs` data structure
* @throws if the provided `lazyInstance` coerces to `null`, or if the `lazyInstance` does not have a `constructor`
* property
*/
export const registerInstance = (
lazyInstance: any,
hostRef: d.HostRef | null | undefined,
): Map<d.RuntimeRef, d.HostRef> => {
export const registerInstance = (lazyInstance: any, hostRef: d.HostRef | null | undefined) => {
if (lazyInstance == null || lazyInstance.constructor == null) {
throw new Error(`Invalid component constructor`);
}
Expand All @@ -44,8 +33,8 @@ export const registerInstance = (
hostRef = getHostRef(elm);
}

lazyInstance.__stencil__getHostRef = () => hostRef;
hostRef.$lazyInstance$ = lazyInstance;
return hostRefs.set(lazyInstance, hostRef);
};

/**
Expand All @@ -65,5 +54,5 @@ export const registerHost = (elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta
hostRef.$onReadyPromise$ = new Promise((r) => (hostRef.$onReadyResolve$ = r));
elm['s-p'] = [];
elm['s-rc'] = [];
hostRefs.set(elm, hostRef);
elm.__stencil__getHostRef = () => hostRef;
};
3 changes: 1 addition & 2 deletions src/testing/platform/testing-platform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type * as d from '@stencil/core/internal';

import { cstrs, hostRefs, moduleLoaded, styles } from './testing-constants';
import { cstrs, moduleLoaded, styles } from './testing-constants';
import { flushAll, resetTaskQueue } from './testing-task-queue';
import { win } from './testing-window';

Expand Down Expand Up @@ -54,7 +54,6 @@ export function resetPlatform(defaults: Partial<d.PlatformRuntime> = {}) {
win.close();
}

hostRefs.clear();
styles.clear();
plt.$flags$ = 0;
Object.assign(plt, defaults);
Expand Down

0 comments on commit 4add75c

Please sign in to comment.