@askrjs/askr 0.0.46 → 0.0.48

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/dist/benchmark.js CHANGED
@@ -10,8 +10,8 @@ import { BenchmarkTable } from "./bench/components/benchmark-table.js";
10
10
  installRendererBridge();
11
11
  const benchmarkMetadata = {
12
12
  packageName: "@askrjs/askr",
13
- packageVersion: "0.0.46",
14
- buildLabel: "0.0.46-local"
13
+ packageVersion: "0.0.48",
14
+ buildLabel: "0.0.48-local"
15
15
  };
16
16
  function getBenchmarkMetadata() {
17
17
  return { ...benchmarkMetadata };
@@ -3,6 +3,31 @@ import { incDevCounter } from "../runtime/dev-namespace.js";
3
3
  import { cleanupComponent } from "../runtime/component.js";
4
4
  import { clearDelegatedHandlersForElement, removeDelegatedListener } from "../runtime/events.js";
5
5
  //#region src/renderer/cleanup.ts
6
+ const elementRefs = /* @__PURE__ */ new WeakMap();
7
+ function applyRefValue(ref, value) {
8
+ const resolvedRef = ref;
9
+ if (!resolvedRef) return;
10
+ if (typeof resolvedRef === "function") {
11
+ resolvedRef(value);
12
+ return;
13
+ }
14
+ if (Object.isExtensible(resolvedRef)) resolvedRef.current = value;
15
+ }
16
+ function updateElementRef(element, ref) {
17
+ const previousRef = elementRefs.get(element);
18
+ if (previousRef === ref) return;
19
+ if (previousRef) applyRefValue(previousRef, null);
20
+ if (ref) {
21
+ applyRefValue(ref, element);
22
+ elementRefs.set(element, ref);
23
+ } else elementRefs.delete(element);
24
+ }
25
+ function removeElementRef(element) {
26
+ const ref = elementRefs.get(element);
27
+ if (!ref) return;
28
+ applyRefValue(ref, null);
29
+ elementRefs.delete(element);
30
+ }
6
31
  function cleanupSingleInstance(node, errors, strict) {
7
32
  const instances = /* @__PURE__ */ new Set();
8
33
  if (Array.isArray(node.__ASKR_INSTANCES)) {
@@ -23,6 +48,12 @@ function cleanupSingleInstance(node, errors, strict) {
23
48
  }
24
49
  }
25
50
  function teardownSingleElement(element, errors, strict) {
51
+ try {
52
+ removeElementRef(element);
53
+ } catch (err) {
54
+ if (strict) errors.push(err);
55
+ else logger.warn("[Askr] removeElementRef failed:", err);
56
+ }
26
57
  try {
27
58
  removeElementListeners(element);
28
59
  } catch (err) {
@@ -167,11 +198,12 @@ function removeElementListeners(element) {
167
198
  function removeAllListeners(root) {
168
199
  if (!root) return;
169
200
  forEachElementInSubtree(root, (el) => {
201
+ removeElementRef(el);
170
202
  removeElementListeners(el);
171
203
  removeElementReactiveProps(el);
172
204
  });
173
205
  }
174
206
  //#endregion
175
- export { REACTIVE_CHILDREN_KEY, cleanupInstanceIfPresent, cleanupInstancesUnder, elementListeners, elementReactivePropsCleanup, removeAllListeners, removeElementListeners, removeElementReactiveProps, teardownNodeSubtree };
207
+ export { REACTIVE_CHILDREN_KEY, cleanupInstanceIfPresent, cleanupInstancesUnder, elementListeners, elementReactivePropsCleanup, elementRefs, removeAllListeners, removeElementListeners, removeElementReactiveProps, removeElementRef, teardownNodeSubtree, updateElementRef };
176
208
 
177
209
  //# sourceMappingURL=cleanup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup.js","names":[],"sources":["../../src/renderer/cleanup.ts"],"sourcesContent":["import { cleanupComponent } from '../runtime/component';\nimport type { ComponentInstance } from '../runtime/component';\nimport { logger } from '../dev/logger';\nimport { incDevCounter } from '../runtime/dev-namespace';\nimport {\n clearDelegatedHandlersForElement,\n removeDelegatedListener,\n} from '../runtime/events';\n\ntype InstanceHost = Node & {\n __ASKR_INSTANCE?: unknown;\n __ASKR_INSTANCES?: unknown[];\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Instance Cleanup Helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction cleanupSingleInstance(\n node: InstanceHost,\n errors: unknown[] | null,\n strict: boolean\n): void {\n const instances = new Set<ComponentInstance>();\n\n if (Array.isArray(node.__ASKR_INSTANCES)) {\n for (const instance of node.__ASKR_INSTANCES) {\n if (instance) {\n instances.add(instance as ComponentInstance);\n }\n }\n }\n\n if (node.__ASKR_INSTANCE) {\n instances.add(node.__ASKR_INSTANCE as ComponentInstance);\n }\n\n for (const instance of instances) {\n try {\n cleanupComponent(instance);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] cleanupComponent failed:', err);\n }\n }\n\n try {\n delete node.__ASKR_INSTANCE;\n delete node.__ASKR_INSTANCES;\n } catch (e) {\n if (strict) errors!.push(e);\n }\n}\n\nfunction teardownSingleElement(\n element: Element,\n errors: unknown[] | null,\n strict: boolean\n): void {\n try {\n removeElementListeners(element);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] removeElementListeners failed:', err);\n }\n\n try {\n removeElementReactiveProps(element);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] removeElementReactiveProps failed:', err);\n }\n\n try {\n cleanupSingleInstance(element as InstanceHost, errors, strict);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] cleanupSingleInstance failed:', err);\n }\n}\n\n// Walk descendant elements with minimal allocations.\n// HOT PATH: used during subtree teardown (replace/unmount).\nfunction forEachDescendantElement(root: Element, visit: (el: Element) => void) {\n // Prefer TreeWalker when available; it avoids allocating a NodeList.\n try {\n const doc = root.ownerDocument;\n const createTreeWalker = doc?.createTreeWalker;\n if (typeof createTreeWalker === 'function') {\n // NodeFilter.SHOW_ELEMENT === 1\n const walker = createTreeWalker.call(doc, root, 1);\n let n = walker.firstChild();\n while (n) {\n visit(n as Element);\n n = walker.nextNode();\n }\n return;\n }\n } catch {\n // SLOW PATH: TreeWalker unavailable\n }\n\n // Fallback: querySelectorAll\n const descendants = root.querySelectorAll('*');\n for (let i = 0; i < descendants.length; i++) {\n visit(descendants[i]);\n }\n}\n\nfunction forEachDescendantNode(root: Node, visit: (node: Node) => void) {\n try {\n const doc = root.ownerDocument;\n const createTreeWalker = doc?.createTreeWalker;\n if (typeof createTreeWalker === 'function') {\n // NodeFilter.SHOW_ALL === 0xffffffff\n const walker = createTreeWalker.call(doc, root, 0xffffffff);\n let node = walker.nextNode();\n while (node) {\n visit(node);\n node = walker.nextNode();\n }\n return;\n }\n } catch {\n // SLOW PATH: TreeWalker unavailable\n }\n\n const stack = Array.from(root.childNodes).reverse();\n while (stack.length > 0) {\n const node = stack.pop()!;\n visit(node);\n for (let child = node.lastChild; child; child = child.previousSibling) {\n stack.push(child);\n }\n }\n}\n\nfunction forEachElementInSubtree(root: Element, visit: (el: Element) => void) {\n visit(root);\n forEachDescendantElement(root, visit);\n}\n\n// Track listeners so we can remove them on cleanup\nexport interface ListenerMapEntry {\n handler: EventListener;\n original: EventListener;\n eventName: string;\n options?: boolean | AddEventListenerOptions;\n isDelegated?: boolean;\n updateHandler?: (nextHandler: EventListener) => void;\n}\nexport const elementListeners = new WeakMap<\n Element,\n Map<string, ListenerMapEntry>\n>();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Public API\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Clean up component instance attached to a DOM node\n * Accepts an optional `opts.strict` flag to surface errors instead of swallowing them.\n */\nexport function cleanupInstanceIfPresent(\n node: Node | null,\n opts?: { strict?: boolean }\n): void {\n if (!node) return;\n\n const strict = opts?.strict ?? false;\n const errors: unknown[] | null = strict ? [] : null;\n\n // Clean up the node itself\n try {\n cleanupSingleInstance(node as InstanceHost, errors, strict);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] cleanupInstanceIfPresent failed:', err);\n }\n\n // Clean up any nested instances, including null-component comment hosts.\n try {\n forEachDescendantNode(node, (descendant) => {\n try {\n cleanupSingleInstance(descendant as InstanceHost, errors, strict);\n } catch (err) {\n if (strict) errors!.push(err);\n else\n logger.warn(\n '[Askr] cleanupInstanceIfPresent descendant cleanup failed:',\n err\n );\n }\n });\n } catch (err) {\n if (strict) errors!.push(err);\n else\n logger.warn(\n '[Askr] cleanupInstanceIfPresent descendant query failed:',\n err\n );\n }\n\n if (errors && errors.length > 0) {\n throw new AggregateError(errors, 'cleanupInstanceIfPresent failed');\n }\n}\n\n// Public helper to clean up any component instances under a node. Used by\n// runtime commit logic to ensure component instances are torn down when their\n// host nodes are removed during an update.\nexport function cleanupInstancesUnder(\n node: Node | null,\n opts?: { strict?: boolean }\n): void {\n cleanupInstanceIfPresent(node, opts);\n}\n\nexport function teardownNodeSubtree(\n node: Node | null,\n opts?: { strict?: boolean }\n): void {\n if (!node) return;\n\n const strict = opts?.strict ?? false;\n const errors: unknown[] | null = strict ? [] : null;\n\n if (!(node instanceof Element)) {\n cleanupSingleInstance(node as InstanceHost, errors, strict);\n if (errors && errors.length > 0) {\n throw new AggregateError(errors, 'teardownNodeSubtree failed');\n }\n return;\n }\n\n try {\n teardownSingleElement(node, errors, strict);\n forEachDescendantNode(node, (descendant) => {\n if (descendant instanceof Element) {\n teardownSingleElement(descendant, errors, strict);\n } else {\n cleanupSingleInstance(descendant as InstanceHost, errors, strict);\n }\n });\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] teardownNodeSubtree failed:', err);\n }\n\n if (errors && errors.length > 0) {\n throw new AggregateError(errors, 'teardownNodeSubtree failed');\n }\n}\n\n// Track reactive props cleanup functions and their function references\nexport interface ReactivePropCleanupEntry {\n cleanup: () => void;\n fnRef: unknown;\n updateFn?: (nextValue: unknown) => void;\n}\n\nexport const REACTIVE_CHILDREN_KEY = '__askr_reactive_children__';\n\nexport const elementReactivePropsCleanup = new WeakMap<\n Element,\n Map<string, ReactivePropCleanupEntry>\n>();\n\nexport function removeElementReactiveProps(element: Element): void {\n const cleanupMap = elementReactivePropsCleanup.get(element);\n if (cleanupMap) {\n for (const entry of cleanupMap.values()) {\n try {\n entry.cleanup();\n } catch (err) {\n logger.warn('[Askr] reactive prop cleanup failed:', err);\n }\n }\n elementReactivePropsCleanup.delete(element);\n }\n}\n\nexport function removeElementListeners(element: Element): void {\n const map = elementListeners.get(element);\n if (map) {\n for (const entry of map.values()) {\n incDevCounter('listenerRemoves');\n if (entry.isDelegated) {\n removeDelegatedListener(element, entry.eventName);\n } else {\n if (entry.options !== undefined)\n element.removeEventListener(\n entry.eventName,\n entry.handler,\n entry.options\n );\n else element.removeEventListener(entry.eventName, entry.handler);\n }\n }\n elementListeners.delete(element);\n }\n\n clearDelegatedHandlersForElement(element);\n}\n\nexport function removeAllListeners(root: Element | null): void {\n if (!root) return;\n\n forEachElementInSubtree(root, (el) => {\n removeElementListeners(el);\n removeElementReactiveProps(el);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cleanup.js","names":[],"sources":["../../src/renderer/cleanup.ts"],"sourcesContent":["import { cleanupComponent } from '../runtime/component';\nimport type { ComponentInstance } from '../runtime/component';\nimport { logger } from '../dev/logger';\nimport { incDevCounter } from '../runtime/dev-namespace';\nimport {\n clearDelegatedHandlersForElement,\n removeDelegatedListener,\n} from '../runtime/events';\n\ntype InstanceHost = Node & {\n __ASKR_INSTANCE?: unknown;\n __ASKR_INSTANCES?: unknown[];\n};\n\ntype Ref<T> =\n | ((value: T | null) => void)\n | { current: T | null }\n | null\n | undefined;\n\nexport const elementRefs = new WeakMap<Element, unknown>();\n\nfunction applyRefValue<T>(ref: unknown, value: T | null): void {\n const resolvedRef = ref as Ref<T>;\n\n if (!resolvedRef) {\n return;\n }\n\n if (typeof resolvedRef === 'function') {\n resolvedRef(value);\n return;\n }\n\n if (Object.isExtensible(resolvedRef)) {\n (resolvedRef as { current: T | null }).current = value;\n }\n}\n\nexport function updateElementRef<T extends Element>(\n element: T,\n ref: unknown\n): void {\n const previousRef = elementRefs.get(element);\n\n if (previousRef === ref) {\n return;\n }\n\n if (previousRef) {\n applyRefValue(previousRef, null);\n }\n\n if (ref) {\n applyRefValue(ref, element);\n elementRefs.set(element, ref);\n } else {\n elementRefs.delete(element);\n }\n}\n\nexport function removeElementRef(element: Element): void {\n const ref = elementRefs.get(element);\n\n if (!ref) {\n return;\n }\n\n applyRefValue(ref, null);\n elementRefs.delete(element);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Instance Cleanup Helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction cleanupSingleInstance(\n node: InstanceHost,\n errors: unknown[] | null,\n strict: boolean\n): void {\n const instances = new Set<ComponentInstance>();\n\n if (Array.isArray(node.__ASKR_INSTANCES)) {\n for (const instance of node.__ASKR_INSTANCES) {\n if (instance) {\n instances.add(instance as ComponentInstance);\n }\n }\n }\n\n if (node.__ASKR_INSTANCE) {\n instances.add(node.__ASKR_INSTANCE as ComponentInstance);\n }\n\n for (const instance of instances) {\n try {\n cleanupComponent(instance);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] cleanupComponent failed:', err);\n }\n }\n\n try {\n delete node.__ASKR_INSTANCE;\n delete node.__ASKR_INSTANCES;\n } catch (e) {\n if (strict) errors!.push(e);\n }\n}\n\nfunction teardownSingleElement(\n element: Element,\n errors: unknown[] | null,\n strict: boolean\n): void {\n try {\n removeElementRef(element);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] removeElementRef failed:', err);\n }\n\n try {\n removeElementListeners(element);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] removeElementListeners failed:', err);\n }\n\n try {\n removeElementReactiveProps(element);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] removeElementReactiveProps failed:', err);\n }\n\n try {\n cleanupSingleInstance(element as InstanceHost, errors, strict);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] cleanupSingleInstance failed:', err);\n }\n}\n\n// Walk descendant elements with minimal allocations.\n// HOT PATH: used during subtree teardown (replace/unmount).\nfunction forEachDescendantElement(root: Element, visit: (el: Element) => void) {\n // Prefer TreeWalker when available; it avoids allocating a NodeList.\n try {\n const doc = root.ownerDocument;\n const createTreeWalker = doc?.createTreeWalker;\n if (typeof createTreeWalker === 'function') {\n // NodeFilter.SHOW_ELEMENT === 1\n const walker = createTreeWalker.call(doc, root, 1);\n let n = walker.firstChild();\n while (n) {\n visit(n as Element);\n n = walker.nextNode();\n }\n return;\n }\n } catch {\n // SLOW PATH: TreeWalker unavailable\n }\n\n // Fallback: querySelectorAll\n const descendants = root.querySelectorAll('*');\n for (let i = 0; i < descendants.length; i++) {\n visit(descendants[i]);\n }\n}\n\nfunction forEachDescendantNode(root: Node, visit: (node: Node) => void) {\n try {\n const doc = root.ownerDocument;\n const createTreeWalker = doc?.createTreeWalker;\n if (typeof createTreeWalker === 'function') {\n // NodeFilter.SHOW_ALL === 0xffffffff\n const walker = createTreeWalker.call(doc, root, 0xffffffff);\n let node = walker.nextNode();\n while (node) {\n visit(node);\n node = walker.nextNode();\n }\n return;\n }\n } catch {\n // SLOW PATH: TreeWalker unavailable\n }\n\n const stack = Array.from(root.childNodes).reverse();\n while (stack.length > 0) {\n const node = stack.pop()!;\n visit(node);\n for (let child = node.lastChild; child; child = child.previousSibling) {\n stack.push(child);\n }\n }\n}\n\nfunction forEachElementInSubtree(root: Element, visit: (el: Element) => void) {\n visit(root);\n forEachDescendantElement(root, visit);\n}\n\n// Track listeners so we can remove them on cleanup\nexport interface ListenerMapEntry {\n handler: EventListener;\n original: EventListener;\n eventName: string;\n options?: boolean | AddEventListenerOptions;\n isDelegated?: boolean;\n updateHandler?: (nextHandler: EventListener) => void;\n}\nexport const elementListeners = new WeakMap<\n Element,\n Map<string, ListenerMapEntry>\n>();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Public API\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Clean up component instance attached to a DOM node\n * Accepts an optional `opts.strict` flag to surface errors instead of swallowing them.\n */\nexport function cleanupInstanceIfPresent(\n node: Node | null,\n opts?: { strict?: boolean }\n): void {\n if (!node) return;\n\n const strict = opts?.strict ?? false;\n const errors: unknown[] | null = strict ? [] : null;\n\n // Clean up the node itself\n try {\n cleanupSingleInstance(node as InstanceHost, errors, strict);\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] cleanupInstanceIfPresent failed:', err);\n }\n\n // Clean up any nested instances, including null-component comment hosts.\n try {\n forEachDescendantNode(node, (descendant) => {\n try {\n cleanupSingleInstance(descendant as InstanceHost, errors, strict);\n } catch (err) {\n if (strict) errors!.push(err);\n else\n logger.warn(\n '[Askr] cleanupInstanceIfPresent descendant cleanup failed:',\n err\n );\n }\n });\n } catch (err) {\n if (strict) errors!.push(err);\n else\n logger.warn(\n '[Askr] cleanupInstanceIfPresent descendant query failed:',\n err\n );\n }\n\n if (errors && errors.length > 0) {\n throw new AggregateError(errors, 'cleanupInstanceIfPresent failed');\n }\n}\n\n// Public helper to clean up any component instances under a node. Used by\n// runtime commit logic to ensure component instances are torn down when their\n// host nodes are removed during an update.\nexport function cleanupInstancesUnder(\n node: Node | null,\n opts?: { strict?: boolean }\n): void {\n cleanupInstanceIfPresent(node, opts);\n}\n\nexport function teardownNodeSubtree(\n node: Node | null,\n opts?: { strict?: boolean }\n): void {\n if (!node) return;\n\n const strict = opts?.strict ?? false;\n const errors: unknown[] | null = strict ? [] : null;\n\n if (!(node instanceof Element)) {\n cleanupSingleInstance(node as InstanceHost, errors, strict);\n if (errors && errors.length > 0) {\n throw new AggregateError(errors, 'teardownNodeSubtree failed');\n }\n return;\n }\n\n try {\n teardownSingleElement(node, errors, strict);\n forEachDescendantNode(node, (descendant) => {\n if (descendant instanceof Element) {\n teardownSingleElement(descendant, errors, strict);\n } else {\n cleanupSingleInstance(descendant as InstanceHost, errors, strict);\n }\n });\n } catch (err) {\n if (strict) errors!.push(err);\n else logger.warn('[Askr] teardownNodeSubtree failed:', err);\n }\n\n if (errors && errors.length > 0) {\n throw new AggregateError(errors, 'teardownNodeSubtree failed');\n }\n}\n\n// Track reactive props cleanup functions and their function references\nexport interface ReactivePropCleanupEntry {\n cleanup: () => void;\n fnRef: unknown;\n updateFn?: (nextValue: unknown) => void;\n}\n\nexport const REACTIVE_CHILDREN_KEY = '__askr_reactive_children__';\n\nexport const elementReactivePropsCleanup = new WeakMap<\n Element,\n Map<string, ReactivePropCleanupEntry>\n>();\n\nexport function removeElementReactiveProps(element: Element): void {\n const cleanupMap = elementReactivePropsCleanup.get(element);\n if (cleanupMap) {\n for (const entry of cleanupMap.values()) {\n try {\n entry.cleanup();\n } catch (err) {\n logger.warn('[Askr] reactive prop cleanup failed:', err);\n }\n }\n elementReactivePropsCleanup.delete(element);\n }\n}\n\nexport function removeElementListeners(element: Element): void {\n const map = elementListeners.get(element);\n if (map) {\n for (const entry of map.values()) {\n incDevCounter('listenerRemoves');\n if (entry.isDelegated) {\n removeDelegatedListener(element, entry.eventName);\n } else {\n if (entry.options !== undefined)\n element.removeEventListener(\n entry.eventName,\n entry.handler,\n entry.options\n );\n else element.removeEventListener(entry.eventName, entry.handler);\n }\n }\n elementListeners.delete(element);\n }\n\n clearDelegatedHandlersForElement(element);\n}\n\nexport function removeAllListeners(root: Element | null): void {\n if (!root) return;\n\n forEachElementInSubtree(root, (el) => {\n removeElementRef(el);\n removeElementListeners(el);\n removeElementReactiveProps(el);\n });\n}\n"],"mappings":"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"}
@@ -13,7 +13,7 @@ import { createMutableWrappedHandler, extractKey, getEventListenerKey, getEventL
13
13
  import { keyedElements } from "./keyed.js";
14
14
  import { captureInlineRenderSnapshot, cleanupComponent, createComponentInstance, getCurrentInstance, mountInstanceInline, renderComponentInline, warnUnusedStateReads } from "../runtime/component.js";
15
15
  import { addDelegatedListener, getDelegatedHandlerForElement, getDelegatedHandlersForElement, isDelegatedEvent, isEventDelegationEnabled, removeDelegatedListener, updateDelegatedListener } from "../runtime/events.js";
16
- import { REACTIVE_CHILDREN_KEY, elementListeners, elementReactivePropsCleanup, removeAllListeners, removeElementListeners, removeElementReactiveProps, teardownNodeSubtree } from "./cleanup.js";
16
+ import { REACTIVE_CHILDREN_KEY, elementListeners, elementReactivePropsCleanup, removeAllListeners, removeElementListeners, removeElementReactiveProps, teardownNodeSubtree, updateElementRef } from "./cleanup.js";
17
17
  import { ROUTE_ROOT_COMPONENT } from "../common/router-internal.js";
18
18
  import "../jsx-runtime.js";
19
19
  import { createChildScope, disposeChildScope, rerenderChildScope } from "../runtime/child-scope.js";
@@ -178,6 +178,31 @@ function cleanupDetachedComponentHost(host, retainedInstance) {
178
178
  delete host.__ASKR_WRAPPER_HOST;
179
179
  } catch {}
180
180
  }
181
+ function pruneComponentHostInstances(host, retainedInstances) {
182
+ const retained = new Set(retainedInstances);
183
+ const nextInstances = [];
184
+ const staleInstances = /* @__PURE__ */ new Set();
185
+ const retainOrMarkStale = (instance) => {
186
+ if (!instance) return;
187
+ if (retained.has(instance)) {
188
+ if (!nextInstances.includes(instance)) nextInstances.push(instance);
189
+ return;
190
+ }
191
+ staleInstances.add(instance);
192
+ };
193
+ for (const instance of host.__ASKR_INSTANCES ?? []) retainOrMarkStale(instance);
194
+ retainOrMarkStale(host.__ASKR_INSTANCE);
195
+ for (const instance of staleInstances) cleanupComponent(instance);
196
+ try {
197
+ if (nextInstances.length > 0) {
198
+ host.__ASKR_INSTANCES = nextInstances;
199
+ host.__ASKR_INSTANCE = nextInstances[0];
200
+ } else {
201
+ delete host.__ASKR_INSTANCES;
202
+ delete host.__ASKR_INSTANCE;
203
+ }
204
+ } catch {}
205
+ }
181
206
  function canUpdateReactiveChildBoundarySequenceSource(previousSource, nextSource) {
182
207
  if (!Array.isArray(previousSource) || previousSource.length !== nextSource.length) return false;
183
208
  for (let index = 0; index < nextSource.length; index += 1) {
@@ -938,7 +963,7 @@ function applyPropsToElement(el, props, tagName) {
938
963
  for (const key in props) {
939
964
  const value = props[key];
940
965
  if (key === "ref") {
941
- applyRef(el, value);
966
+ updateElementRef(el, value);
942
967
  continue;
943
968
  }
944
969
  if (isSkippedProp(key)) continue;
@@ -966,15 +991,6 @@ function applyPropsToElement(el, props, tagName) {
966
991
  else setRenderedAttribute(el, key, String(value));
967
992
  }
968
993
  }
969
- function applyRef(el, ref) {
970
- const r = ref;
971
- if (!r) return;
972
- if (typeof r === "function") {
973
- r(el);
974
- return;
975
- }
976
- if (Object.isExtensible(r)) r.current = el;
977
- }
978
994
  function removeStaleAttributes(el, vnode, props) {
979
995
  const desiredAttributes = /* @__PURE__ */ new Set();
980
996
  if (extractKey(vnode) !== void 0) desiredAttributes.add("data-key");
@@ -1325,6 +1341,7 @@ function syncComponentElement(currentDom, node, type, props, parentNamespace, fo
1325
1341
  if (isPromiseLike(result)) throw new Error("Async components are not supported. Components must return synchronously.");
1326
1342
  const scopedResult = markVNodeTreeWithContextFrame(result, snapshot ?? null);
1327
1343
  if (scopedResult && typeof scopedResult === "object" && "type" in scopedResult && typeof scopedResult.type === "string" && tagNamesEqualIgnoreCase(existingHost.tagName, scopedResult.type)) {
1344
+ pruneComponentHostInstances(existingHost, retainedHostInstances ? [hydrationInstance, ...retainedHostInstances] : [hydrationInstance]);
1328
1345
  withContext(snapshot, () => {
1329
1346
  updateElementFromVnode(existingHost, inheritComponentKey(scopedResult, node), true, forceChildrenUpdate || hydrationInstance.mounted === false);
1330
1347
  materializeKey(existingHost, node, props);
@@ -1370,6 +1387,7 @@ function syncComponentElement(currentDom, node, type, props, parentNamespace, fo
1370
1387
  return existingHost;
1371
1388
  }
1372
1389
  if (scopedResult && typeof scopedResult === "object" && "type" in scopedResult && typeof scopedResult.type === "string" && tagNamesEqualIgnoreCase(existingHost.tagName, scopedResult.type)) {
1390
+ pruneComponentHostInstances(existingHost, retainedHostInstances ? [existingInstance, ...retainedHostInstances] : [existingInstance]);
1373
1391
  withContext(snapshot, () => {
1374
1392
  updateElementFromVnode(existingHost, inheritComponentKey(scopedResult, node), true, forceChildrenUpdate || existingInstance.mounted === false);
1375
1393
  materializeKey(existingHost, node, props);
@@ -1767,6 +1785,7 @@ function updateElementFromVnode(el, vnode, updateChildren = true, forceChildrenU
1767
1785
  return;
1768
1786
  }
1769
1787
  materializeKey(el, vnode, props);
1788
+ updateElementRef(el, props.ref);
1770
1789
  const existingListeners = elementListeners.get(el);
1771
1790
  const existingReactiveProps = elementReactivePropsCleanup.get(el);
1772
1791
  if ((!existingListeners || existingListeners.size === 0) && (!existingReactiveProps || existingReactiveProps.size === 0)) {
@@ -1786,6 +1805,7 @@ function updateElementFromVnode(el, vnode, updateChildren = true, forceChildrenU
1786
1805
  if (usesReactiveChildren) (desiredReactivePropNames ??= /* @__PURE__ */ new Set()).add(REACTIVE_CHILDREN_KEY);
1787
1806
  for (const key in props) {
1788
1807
  const value = props[key];
1808
+ if (key === "ref") continue;
1789
1809
  if (isSkippedProp(key)) continue;
1790
1810
  const eventProp = parseEventProp(key);
1791
1811
  const eventName = eventProp?.eventName;