@adobe-commerce/elsie 2.1.0-beta.4 → 2.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @adobe-commerce/elsie
2
2
 
3
+ ## 2.1.0-beta.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 4cc7219: Fix a memory leak in `Slot`'s VNode cache (added to fix repeated
8
+ `insertBefore` crashes from replay churn): entries were never pruned, so a
9
+ `slot` callback that keeps calling `replaceWith`/`appendChild`/`prependChild`
10
+ with newly constructed elements over time (rather than mutating one element in
11
+ place) would pin every historical element in memory for the Slot's whole
12
+ lifetime. Entries are now evicted via the grafted element's `ref` callback
13
+ firing with `null`, which Preact does precisely when that VNode is detached
14
+ (superseded by a different element, or the Slot unmounting).
15
+
16
+ See `docs/slot-dom-graft-race.md` for the full trace, including a
17
+ pre-existing, unrelated gap this surfaced: calling `replaceWith` a second time
18
+ with a different element doesn't remove the first one.
19
+
20
+ - ba3bbe7: Fix a recurring `insertBefore` crash in `Slot` when content grafted
21
+ via `replaceWith`/`appendChild`/`prependChild` is re-applied on unrelated
22
+ re-renders. `Slot` replays its registered methods on every render pass (by
23
+ design, so `onRender`/`onChange` can react to fresh state), but the grafted
24
+ content's wrapper VNode was rebuilt from scratch on every replay, handing
25
+ Preact a brand new `ref` closure for a DOM node that was already grafted. That
26
+ churn corrupted Preact's internal DOM bookkeeping for the grafted subtree over
27
+ repeated re-renders, especially in components with ongoing state changes. The
28
+ wrapper VNode is now cached per element, so repeated replays reuse the same
29
+ VNode instance and Preact's diff bails out instead of re-touching the subtree.
30
+
31
+ See `docs/slot-dom-graft-race.md` for the full trace, including why an earlier
32
+ attempt at this fix (making `replaceWith` a no-op after its first run) was
33
+ wrong.
34
+
3
35
  ## 2.1.0-beta.4
4
36
 
5
37
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "2.1.0-beta.4",
3
+ "version": "2.1.0-beta.5",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
package/src/lib/slot.tsx CHANGED
@@ -169,20 +169,59 @@ export function useSlot<K, V extends HTMLElement>(
169
169
  // @ts-ignore
170
170
  context._registerMethod = _registerMethod;
171
171
 
172
+ // `onRender`-style registered methods are replayed on every
173
+ // `handleLifeCycleRender` pass (see `_registerMethod`/`methodsRef` above),
174
+ // and `handleLifeCycleRender` always resets `children` to the fallback
175
+ // before replaying them -- so `replaceWith`/`appendChild`/`prependChild`
176
+ // must genuinely re-apply their VNode on every replay, not just once.
177
+ // Without caching, each replay would call `createElement` fresh, handing
178
+ // Preact a brand new VNode (and new `ref` identity) for content that's
179
+ // already grafted, forcing it to detach/reattach the `ref` for no reason.
180
+ // Caching per `elem` means repeat calls return the *same* VNode object;
181
+ // Preact's diff treats `oldVNode === newVNode` as an explicit "unchanged,
182
+ // skip" signal, so it never re-touches the grafted subtree. See
183
+ // docs/slot-dom-graft-race.md.
184
+ const htmlElementVNodeCache = useMemo(
185
+ () => new Map<HTMLElement, VNode<any>>(),
186
+ // `contentTag` isn't read by the factory -- it's a deliberate cache-bust
187
+ // key, so a `contentTag` change starts a fresh cache instead of handing
188
+ // out VNodes built for the old wrapper tag.
189
+ // eslint-disable-next-line react-hooks/exhaustive-deps
190
+ [contentTag],
191
+ );
192
+
172
193
  const _htmlElementToVNode = useCallback(
173
194
  (elem: HTMLElement) => {
174
- return createElement(
195
+ const cached = htmlElementVNodeCache.get(elem);
196
+ if (cached) return cached;
197
+
198
+ const vnode = createElement(
175
199
  contentTag,
176
200
  {
177
201
  'data-slot-html-element': elem.tagName.toLowerCase(),
178
202
  ref: (refElem: HTMLElement | null): void => {
179
- refElem?.appendChild(elem);
203
+ if (refElem) {
204
+ refElem.appendChild(elem);
205
+ return;
206
+ }
207
+
208
+ // Preact calls `ref` with `null` when this specific VNode is
209
+ // detached -- either superseded by a different element at the
210
+ // same position (a later replaceWith/appendChild/prependChild
211
+ // call) or because the Slot unmounted entirely. Either way,
212
+ // `elem` is no longer part of the tree, so drop it from the
213
+ // cache now instead of holding onto it (and the DOM node it
214
+ // wraps) for the Slot's remaining lifetime.
215
+ htmlElementVNodeCache.delete(elem);
180
216
  },
181
217
  },
182
218
  null,
183
219
  );
220
+
221
+ htmlElementVNodeCache.set(elem, vnode);
222
+ return vnode;
184
223
  },
185
- [contentTag],
224
+ [contentTag, htmlElementVNodeCache],
186
225
  );
187
226
 
188
227
  // @ts-ignore