assign-gingerly 0.0.28 → 0.0.29

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/README.md CHANGED
@@ -1427,6 +1427,150 @@ element.enh.dispose(registryItem); // Stops timer and cleans up
1427
1427
  - Calling `enh.get()` again will create a new instance
1428
1428
  - The enhancement property is removed from the enh container
1429
1429
 
1430
+ #### Memory Management and When to Call Dispose
1431
+
1432
+ **Important: Understanding automatic vs manual cleanup**
1433
+
1434
+ The enhancement storage system uses a **WeakMap** to prevent memory leaks:
1435
+
1436
+ ```TypeScript
1437
+ // Global storage: WeakMap<Element, Map<EnhancementConfig, Instance>>
1438
+ ```
1439
+
1440
+ **What this means for memory:**
1441
+
1442
+ ✅ **Automatic cleanup when elements are garbage collected:**
1443
+ - When an element is GC'd, the WeakMap entry is automatically removed
1444
+ - Both `enhKey` references (`element.enh[enhKey]`) and WeakMap entries are cleaned up
1445
+ - **No memory leak from the storage mechanism itself**
1446
+
1447
+ ⚠️ **Manual cleanup needed for enhancement internals:**
1448
+ - Event listeners on global objects (window, document)
1449
+ - Timers (setInterval, setTimeout)
1450
+ - External registries or caches
1451
+ - Network connections or subscriptions
1452
+
1453
+ **The challenge: Knowing WHEN to dispose**
1454
+
1455
+ JavaScript provides no way to detect when an element is about to be garbage collected. Additionally, DOM disconnection doesn't reliably indicate disposal:
1456
+
1457
+ ```TypeScript
1458
+ // Element disconnected - but should we dispose?
1459
+ element.remove();
1460
+
1461
+ // Case 1: Temporarily removed, will be re-added
1462
+ setTimeout(() => document.body.append(element), 1000);
1463
+ // ❌ Don't dispose - enhancement should persist
1464
+
1465
+ // Case 2: Moved to another location
1466
+ otherContainer.append(element);
1467
+ // ❌ Don't dispose - enhancement should persist
1468
+
1469
+ // Case 3: Cached for reuse
1470
+ elementCache.set('myElement', element);
1471
+ // ❌ Don't dispose - enhancement should persist
1472
+
1473
+ // Case 4: Truly done, ready for GC
1474
+ element = null;
1475
+ // ✅ Should dispose, but no way to detect this automatically
1476
+ ```
1477
+
1478
+ **Practical disposal strategies:**
1479
+
1480
+ 1. **Short-lived elements:** Don't worry about disposal - WeakMap handles cleanup automatically when elements are GC'd
1481
+
1482
+ 2. **Long-lived applications:** Implement manual disposal at logical boundaries:
1483
+ ```TypeScript
1484
+ // On route change
1485
+ router.beforeLeave(() => {
1486
+ oldRouteElements.forEach(el => el.enh.dispose(registryItem));
1487
+ });
1488
+
1489
+ // On explicit user action
1490
+ closeButton.onclick = () => {
1491
+ dialog.enh.dispose(registryItem);
1492
+ dialog.remove();
1493
+ };
1494
+ ```
1495
+
1496
+ 3. **Framework integration:** Use framework lifecycle hooks:
1497
+ ```TypeScript
1498
+ // React
1499
+ useEffect(() => {
1500
+ return () => elementRef.current?.enh.dispose(registryItem);
1501
+ }, []);
1502
+
1503
+ // Vue
1504
+ onUnmounted(() => {
1505
+ element.value?.enh.dispose(registryItem);
1506
+ });
1507
+ ```
1508
+
1509
+ 4. **MutationObserver heuristic:** Watch for disconnection + timeout (imperfect but practical):
1510
+ ```TypeScript
1511
+ const observer = new MutationObserver(() => {
1512
+ if (!element.isConnected) {
1513
+ setTimeout(() => {
1514
+ if (!element.isConnected) {
1515
+ element.enh.dispose(registryItem);
1516
+ }
1517
+ }, 5000); // If still disconnected after 5s, probably done
1518
+ }
1519
+ });
1520
+ ```
1521
+
1522
+ **Best practices for enhancement authors:**
1523
+
1524
+ Always implement proper cleanup in your dispose method:
1525
+
1526
+ ```TypeScript
1527
+ class MyEnhancement {
1528
+ element;
1529
+ timerId = null;
1530
+ boundHandler = null;
1531
+
1532
+ constructor(element, ctx) {
1533
+ this.element = element;
1534
+ this.boundHandler = this.handleClick.bind(this);
1535
+
1536
+ // Local listener - OK, will be GC'd with element
1537
+ element.addEventListener('click', this.boundHandler);
1538
+
1539
+ // Global listener - MUST clean up manually
1540
+ window.addEventListener('resize', this.boundHandler);
1541
+
1542
+ // Timer - MUST clean up manually
1543
+ this.timerId = setInterval(() => this.update(), 1000);
1544
+ }
1545
+
1546
+ dispose() {
1547
+ // Clean up global listener
1548
+ if (this.boundHandler) {
1549
+ window.removeEventListener('resize', this.boundHandler);
1550
+ }
1551
+
1552
+ // Clean up timer
1553
+ if (this.timerId) {
1554
+ clearInterval(this.timerId);
1555
+ this.timerId = null;
1556
+ }
1557
+
1558
+ // Clear references
1559
+ this.element = null;
1560
+ this.boundHandler = null;
1561
+ }
1562
+
1563
+ handleClick() { /* ... */ }
1564
+ update() { /* ... */ }
1565
+ }
1566
+ ```
1567
+
1568
+ **Summary:**
1569
+ - ✅ Storage mechanism prevents memory leaks via WeakMap
1570
+ - ⚠️ Enhancement internals need manual cleanup via dispose()
1571
+ - ❌ No automatic way to detect when disposal should happen
1572
+ - 👍 Choose disposal strategy based on your application's lifecycle
1573
+
1430
1574
  ### Waiting for Async Initialization with `enh.whenResolved(regItem)`
1431
1575
 
1432
1576
  The `enh.whenResolved(regItem)` method provides a way to wait for asynchronous enhancement initialization:
package/getHost.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Get the itemscope host element for a given element.
3
+ * This function finds the closest element with an itemscope attribute and waits for it to be ready.
4
+ *
5
+ * @param el - The element to start searching from
6
+ * @returns The itemscope host element, or null if none found
7
+ */
8
+ export async function getHost(el) {
9
+ const itemScopeHost = el.closest('[itemscope]');
10
+ if (itemScopeHost) {
11
+ const { localName } = itemScopeHost;
12
+ // If it's a custom element, wait for it to be defined
13
+ if (localName.includes('-')) {
14
+ const registry = itemScopeHost.customElementRegistry ?? customElements;
15
+ await registry.whenDefined(localName);
16
+ return itemScopeHost;
17
+ }
18
+ else {
19
+ // Check if itemscope specifies a value (manager name)
20
+ const itemscopeValue = itemScopeHost.getAttribute('itemscope');
21
+ if (itemscopeValue && itemscopeValue.length > 0) {
22
+ // Wait for the manager to be defined in the itemscope registry
23
+ const registry = itemScopeHost.customElementRegistry?.itemscopeRegistry
24
+ ?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
25
+ if (registry) {
26
+ await registry.whenDefined(itemscopeValue);
27
+ }
28
+ }
29
+ return itemScopeHost;
30
+ }
31
+ }
32
+ else {
33
+ // No itemscope host found in the light DOM
34
+ // Check if we're inside a shadow root and get the shadow host
35
+ const rootNode = el.getRootNode();
36
+ // Check if it's a shadow root (has a host property)
37
+ if (rootNode && 'host' in rootNode && rootNode.host) {
38
+ const host = rootNode.host;
39
+ const { localName } = host;
40
+ // If the host is a custom element, wait for it to be defined
41
+ if (localName.includes('-')) {
42
+ const registry = host.customElementRegistry ?? customElements;
43
+ await registry.whenDefined(localName);
44
+ }
45
+ return host;
46
+ }
47
+ // Not in a shadow root either
48
+ return null;
49
+ }
50
+ }
51
+ export default getHost;
package/getHost.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Get the itemscope host element for a given element.
3
+ * This function finds the closest element with an itemscope attribute and waits for it to be ready.
4
+ *
5
+ * @param el - The element to start searching from
6
+ * @returns The itemscope host element, or null if none found
7
+ */
8
+ export async function getHost(el: Element): Promise<Element | null> {
9
+ const itemScopeHost = el.closest('[itemscope]');
10
+ if (itemScopeHost) {
11
+ const { localName } = itemScopeHost;
12
+
13
+ // If it's a custom element, wait for it to be defined
14
+ if (localName.includes('-')) {
15
+ const registry = (itemScopeHost as any).customElementRegistry ?? customElements;
16
+ await registry.whenDefined(localName);
17
+ return itemScopeHost;
18
+ } else {
19
+ // Check if itemscope specifies a value (manager name)
20
+ const itemscopeValue = itemScopeHost.getAttribute('itemscope');
21
+
22
+ if (itemscopeValue && itemscopeValue.length > 0) {
23
+ // Wait for the manager to be defined in the itemscope registry
24
+ const registry = (itemScopeHost as any).customElementRegistry?.itemscopeRegistry
25
+ ?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
26
+
27
+ if (registry) {
28
+ await registry.whenDefined(itemscopeValue);
29
+ }
30
+ }
31
+
32
+ return itemScopeHost;
33
+ }
34
+ } else {
35
+ // No itemscope host found in the light DOM
36
+ // Check if we're inside a shadow root and get the shadow host
37
+ const rootNode = el.getRootNode();
38
+
39
+ // Check if it's a shadow root (has a host property)
40
+ if (rootNode && 'host' in rootNode && (rootNode as ShadowRoot).host) {
41
+ const host = (rootNode as ShadowRoot).host;
42
+ const { localName } = host;
43
+
44
+ // If the host is a custom element, wait for it to be defined
45
+ if (localName.includes('-')) {
46
+ const registry = (host as any).customElementRegistry ?? customElements;
47
+ await registry.whenDefined(localName);
48
+ }
49
+
50
+ return host;
51
+ }
52
+
53
+ // Not in a shadow root either
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export default getHost;
package/index.js CHANGED
@@ -6,4 +6,5 @@ export { ParserRegistry, globalParserRegistry } from './parserRegistry.js';
6
6
  export { parseWithAttrs } from './parseWithAttrs.js';
7
7
  export { buildCSSQuery } from './buildCSSQuery.js';
8
8
  export { resolveTemplate } from './resolveTemplate.js';
9
+ export { getHost } from './getHost.js';
9
10
  import './object-extension.js';
package/index.ts CHANGED
@@ -6,4 +6,5 @@ export {ParserRegistry, globalParserRegistry} from './parserRegistry.js';
6
6
  export {parseWithAttrs} from './parseWithAttrs.js';
7
7
  export {buildCSSQuery} from './buildCSSQuery.js';
8
8
  export {resolveTemplate} from './resolveTemplate.js';
9
+ export {getHost} from './getHost.js';
9
10
  import './object-extension.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -51,6 +51,10 @@
51
51
  "./resolveTemplate.js": {
52
52
  "default": "./resolveTemplate.js",
53
53
  "types": "./resolveTemplate.ts"
54
+ },
55
+ "./getHost.js": {
56
+ "default": "./getHost.js",
57
+ "types": "./getHost.ts"
54
58
  }
55
59
  },
56
60
  "main": "index.js",
@@ -112,7 +112,7 @@ export type ParserFunction<T = any> =
112
112
  | ((attrValue: string | null) => any)
113
113
  | ((attrValue: string | null, context?: ParserContext<T>) => any);
114
114
 
115
- export interface AttrConfig<T = any> {
115
+ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
116
116
  /**
117
117
  * Type of the property value (JSON-serializable string format)
118
118
  */
@@ -152,6 +152,12 @@ export interface AttrConfig<T = any> {
152
152
  | ParserFunction<T>
153
153
  | string
154
154
  ;
155
+
156
+ /**
157
+ * configuration information needed by a custom parser to properly
158
+ * parse the attribute.
159
+ */
160
+ parserConfig?: TParserConfig;
155
161
 
156
162
  /**
157
163
  * Default value to use when attribute is missing