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 +144 -0
- package/getHost.js +51 -0
- package/getHost.ts +58 -0
- package/index.js +1 -0
- package/index.ts +1 -0
- package/package.json +5 -1
- package/types/assign-gingerly/types.d.ts +7 -1
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.
|
|
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 =
|
|
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
|