mount-observer 0.1.11 → 0.1.13
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/DefineCustomElementHandler.js +99 -98
- package/ElementMountExtension.js +183 -8
- package/ElementMountExtension.ts +218 -11
- package/EnhanceMountedElementHandler.js +96 -95
- package/Events.js +18 -18
- package/Events.ts +6 -6
- package/EvtRt.js +24 -17
- package/EvtRt.ts +30 -18
- package/MountObserver.js +296 -81
- package/MountObserver.ts +387 -121
- package/README.md +1508 -235
- package/RegistryMountCoordinator.js +125 -0
- package/RegistryMountCoordinator.ts +181 -0
- package/connectionMonitor.js +116 -0
- package/connectionMonitor.ts +164 -0
- package/elementIntersection.js +67 -0
- package/elementIntersection.ts +96 -0
- package/{getRootRegistryContainer.js → getRegistryRoot.js} +1 -1
- package/{getRootRegistryContainer.ts → getRegistryRoot.ts} +1 -1
- package/index.js +15 -10
- package/index.ts +15 -10
- package/mediaQuery.js +1 -1
- package/mediaQuery.ts +1 -1
- package/observedRootHas.js +87 -0
- package/package.json +67 -61
- package/playwright.config.ts +1 -0
- package/rootSizeObserver.js +124 -0
- package/rootSizeObserver.ts +157 -0
- package/upShadowSearch.js +64 -0
- package/upShadowSearch.ts +62 -0
- package/DefineCustomElementHandler.ts +0 -116
- package/EnhanceMountedElementHandler.ts +0 -110
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinates MountObserver instances across multiple DOM scopes that share
|
|
3
|
+
* the same CustomElementRegistry. This enables "mutually assured observing"
|
|
4
|
+
* where all scopes with the same registry share mount observers.
|
|
5
|
+
*/
|
|
6
|
+
const regObsGuid = 'iqj6MOueu0OP4CQi1a_4Sw';
|
|
7
|
+
/**
|
|
8
|
+
* Maps CustomElementRegistry -> Map<MountConfig, WeakMap<Node, ObserverEntry>>
|
|
9
|
+
* The MountConfig object itself is used as the key (object identity).
|
|
10
|
+
* The innermost WeakMap maps registry root nodes to their observer entries.
|
|
11
|
+
*/
|
|
12
|
+
export function getRegistryObservers() {
|
|
13
|
+
if (!globalThis[regObsGuid]) {
|
|
14
|
+
globalThis[regObsGuid] = new WeakMap();
|
|
15
|
+
}
|
|
16
|
+
return globalThis[regObsGuid];
|
|
17
|
+
}
|
|
18
|
+
const registryScopeId = 'pt9dS-V7U0SC3Yk708_5Ww';
|
|
19
|
+
/**
|
|
20
|
+
* Tracks all registry root nodes for each CustomElementRegistry.
|
|
21
|
+
* Used to iterate over all scopes when a new config is added.
|
|
22
|
+
*/
|
|
23
|
+
export function getRegistryScopes() {
|
|
24
|
+
if (!globalThis[registryScopeId]) {
|
|
25
|
+
globalThis[registryScopeId] = new WeakMap();
|
|
26
|
+
}
|
|
27
|
+
return globalThis[registryScopeId];
|
|
28
|
+
}
|
|
29
|
+
// const registryScopes = new WeakMap<
|
|
30
|
+
// CustomElementRegistry,
|
|
31
|
+
// WeakDual<Node>
|
|
32
|
+
// >();
|
|
33
|
+
// Note: assignGingerly.ts already has a polyfill for getOrInsertComputed.
|
|
34
|
+
// If this code will already have imported assignGingerly, then no need for the duplicate polyfill below.
|
|
35
|
+
// Polyfill for Map.prototype.getOrInsertComputed and WeakMap.prototype.getOrInsertComputed
|
|
36
|
+
if (typeof Map.prototype.getOrInsertComputed !== 'function') {
|
|
37
|
+
Map.prototype.getOrInsertComputed = function (key, insert) {
|
|
38
|
+
if (this.has(key))
|
|
39
|
+
return this.get(key);
|
|
40
|
+
const value = insert();
|
|
41
|
+
this.set(key, value);
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (typeof WeakMap.prototype.getOrInsertComputed !== 'function') {
|
|
46
|
+
WeakMap.prototype.getOrInsertComputed = function (key, insert) {
|
|
47
|
+
if (this.has(key))
|
|
48
|
+
return this.get(key);
|
|
49
|
+
const value = insert();
|
|
50
|
+
this.set(key, value);
|
|
51
|
+
return value;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Helper to create an observer entry asynchronously.
|
|
56
|
+
* Separated to handle async operations cleanly.
|
|
57
|
+
*/
|
|
58
|
+
async function createObserverEntry(config, registryRoot) {
|
|
59
|
+
// Dynamically import to avoid circular dependency
|
|
60
|
+
const { MountObserver: MountObserverClass } = await import('./MountObserver.js');
|
|
61
|
+
const observer = new MountObserverClass(config);
|
|
62
|
+
await observer.observe(registryRoot);
|
|
63
|
+
return {
|
|
64
|
+
config,
|
|
65
|
+
registryRootRef: new WeakRef(registryRoot),
|
|
66
|
+
observer
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get or create a mount observer for a specific registry + config + registry root combination.
|
|
71
|
+
* This function ensures that:
|
|
72
|
+
* 1. The config is registered with the registry's mountConfigRegistry
|
|
73
|
+
* 2. An observer exists for this specific registry root
|
|
74
|
+
* 3. All other registry roots with the same registry get observers for this config
|
|
75
|
+
* 4. All other configs get observers for this registry root
|
|
76
|
+
*
|
|
77
|
+
* @returns The ObserverEntry for the requested combination
|
|
78
|
+
*/
|
|
79
|
+
export async function getOrInsertObserverEntry(registry, config, registryRoot) {
|
|
80
|
+
// Add config to the registry's config list (if not already there)
|
|
81
|
+
registry.mountConfigRegistry.push(config);
|
|
82
|
+
// Get or create the nested map structure
|
|
83
|
+
const mountConfigMap = (getRegistryObservers()).getOrInsertComputed(registry, () => new Map());
|
|
84
|
+
const nodeToObserverMap = mountConfigMap.getOrInsertComputed(config, () => new WeakMap());
|
|
85
|
+
// Get or create the observer for this specific registry root
|
|
86
|
+
let observerEntry = nodeToObserverMap.get(registryRoot);
|
|
87
|
+
if (!observerEntry) {
|
|
88
|
+
observerEntry = await createObserverEntry(config, registryRoot);
|
|
89
|
+
nodeToObserverMap.set(registryRoot, observerEntry);
|
|
90
|
+
}
|
|
91
|
+
// Track this registry root in the scopes set
|
|
92
|
+
const scopes = getRegistryScopes().getOrInsertComputed(registry, () => ({
|
|
93
|
+
weakSet: new WeakSet(),
|
|
94
|
+
setWeak: new Set()
|
|
95
|
+
}));
|
|
96
|
+
// Add to tracking sets if not already present
|
|
97
|
+
if (!scopes.weakSet.has(registryRoot)) {
|
|
98
|
+
scopes.weakSet.add(registryRoot);
|
|
99
|
+
scopes.setWeak.add(new WeakRef(registryRoot));
|
|
100
|
+
}
|
|
101
|
+
// Get all configs for this registry
|
|
102
|
+
const configs = registry.mountConfigRegistry.items;
|
|
103
|
+
// Iterate over all known registry roots for this registry
|
|
104
|
+
const arr = Array.from(scopes.setWeak);
|
|
105
|
+
for (const regRootRef of arr) {
|
|
106
|
+
const regRoot = regRootRef.deref();
|
|
107
|
+
if (regRoot === undefined)
|
|
108
|
+
continue;
|
|
109
|
+
// For each config, ensure an observer exists for this registry root
|
|
110
|
+
for (const conf of configs) {
|
|
111
|
+
// Skip if this is the same config + root we just created
|
|
112
|
+
if (conf === config && registryRoot === regRoot)
|
|
113
|
+
continue;
|
|
114
|
+
// Get or create observer for this conf + regRoot combination
|
|
115
|
+
// This won't cause infinite loop because we only create if missing
|
|
116
|
+
const confObserverMap = mountConfigMap.getOrInsertComputed(conf, () => new WeakMap());
|
|
117
|
+
let existingEntry = confObserverMap.get(regRoot);
|
|
118
|
+
if (!existingEntry) {
|
|
119
|
+
existingEntry = await createObserverEntry(conf, regRoot);
|
|
120
|
+
confObserverMap.set(regRoot, existingEntry);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return observerEntry;
|
|
125
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinates MountObserver instances across multiple DOM scopes that share
|
|
3
|
+
* the same CustomElementRegistry. This enables "mutually assured observing"
|
|
4
|
+
* where all scopes with the same registry share mount observers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MountConfig, WeakDual } from './types/mount-observer/types.js';
|
|
8
|
+
import type { MountObserver } from './MountObserver.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents a single MountObserver observing a specific registry root.
|
|
12
|
+
*/
|
|
13
|
+
type ObserverEntry = {
|
|
14
|
+
config: MountConfig; // Store for reference
|
|
15
|
+
registryRootRef: WeakRef<Node>;
|
|
16
|
+
observer: MountObserver;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const regObsGuid = 'iqj6MOueu0OP4CQi1a_4Sw';
|
|
20
|
+
|
|
21
|
+
export type CER2MC2N2OE = WeakMap<
|
|
22
|
+
CustomElementRegistry,
|
|
23
|
+
Map<
|
|
24
|
+
MountConfig,
|
|
25
|
+
WeakMap<Node, ObserverEntry>
|
|
26
|
+
>
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maps CustomElementRegistry -> Map<MountConfig, WeakMap<Node, ObserverEntry>>
|
|
31
|
+
* The MountConfig object itself is used as the key (object identity).
|
|
32
|
+
* The innermost WeakMap maps registry root nodes to their observer entries.
|
|
33
|
+
*/
|
|
34
|
+
export function getRegistryObservers(): CER2MC2N2OE{
|
|
35
|
+
if (!(globalThis as any)[regObsGuid]) {
|
|
36
|
+
(globalThis as any)[regObsGuid] = new WeakMap<CustomElementRegistry,
|
|
37
|
+
Map<
|
|
38
|
+
MountConfig,
|
|
39
|
+
WeakMap<Node, ObserverEntry>
|
|
40
|
+
>
|
|
41
|
+
>();
|
|
42
|
+
}
|
|
43
|
+
return (globalThis as any)[regObsGuid] as CER2MC2N2OE;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const registryScopeId = 'pt9dS-V7U0SC3Yk708_5Ww';
|
|
48
|
+
|
|
49
|
+
export type CER2WD = WeakMap<
|
|
50
|
+
CustomElementRegistry,
|
|
51
|
+
WeakDual<Node>
|
|
52
|
+
>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Tracks all registry root nodes for each CustomElementRegistry.
|
|
56
|
+
* Used to iterate over all scopes when a new config is added.
|
|
57
|
+
*/
|
|
58
|
+
export function getRegistryScopes(): CER2WD{
|
|
59
|
+
if (!(globalThis as any)[registryScopeId]) {
|
|
60
|
+
(globalThis as any)[registryScopeId] = new WeakMap<
|
|
61
|
+
CustomElementRegistry,
|
|
62
|
+
WeakDual<Node>
|
|
63
|
+
>();
|
|
64
|
+
}
|
|
65
|
+
return (globalThis as any)[registryScopeId] as CER2WD;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// const registryScopes = new WeakMap<
|
|
70
|
+
// CustomElementRegistry,
|
|
71
|
+
// WeakDual<Node>
|
|
72
|
+
// >();
|
|
73
|
+
|
|
74
|
+
// Note: assignGingerly.ts already has a polyfill for getOrInsertComputed.
|
|
75
|
+
// If this code will already have imported assignGingerly, then no need for the duplicate polyfill below.
|
|
76
|
+
|
|
77
|
+
// Polyfill for Map.prototype.getOrInsertComputed and WeakMap.prototype.getOrInsertComputed
|
|
78
|
+
if (typeof Map.prototype.getOrInsertComputed !== 'function') {
|
|
79
|
+
(Map.prototype as any).getOrInsertComputed = function(key: any, insert: () => any) {
|
|
80
|
+
if (this.has(key)) return this.get(key);
|
|
81
|
+
const value = insert();
|
|
82
|
+
this.set(key, value);
|
|
83
|
+
return value;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (typeof WeakMap.prototype.getOrInsertComputed !== 'function') {
|
|
87
|
+
(WeakMap.prototype as any).getOrInsertComputed = function(key: any, insert: () => any) {
|
|
88
|
+
if (this.has(key)) return this.get(key);
|
|
89
|
+
const value = insert();
|
|
90
|
+
this.set(key, value);
|
|
91
|
+
return value;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Helper to create an observer entry asynchronously.
|
|
97
|
+
* Separated to handle async operations cleanly.
|
|
98
|
+
*/
|
|
99
|
+
async function createObserverEntry(
|
|
100
|
+
config: MountConfig,
|
|
101
|
+
registryRoot: Node
|
|
102
|
+
): Promise<ObserverEntry> {
|
|
103
|
+
// Dynamically import to avoid circular dependency
|
|
104
|
+
const { MountObserver: MountObserverClass } = await import('./MountObserver.js');
|
|
105
|
+
const observer = new MountObserverClass(config);
|
|
106
|
+
await observer.observe(registryRoot);
|
|
107
|
+
return {
|
|
108
|
+
config,
|
|
109
|
+
registryRootRef: new WeakRef(registryRoot),
|
|
110
|
+
observer
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get or create a mount observer for a specific registry + config + registry root combination.
|
|
116
|
+
* This function ensures that:
|
|
117
|
+
* 1. The config is registered with the registry's mountConfigRegistry
|
|
118
|
+
* 2. An observer exists for this specific registry root
|
|
119
|
+
* 3. All other registry roots with the same registry get observers for this config
|
|
120
|
+
* 4. All other configs get observers for this registry root
|
|
121
|
+
*
|
|
122
|
+
* @returns The ObserverEntry for the requested combination
|
|
123
|
+
*/
|
|
124
|
+
export async function getOrInsertObserverEntry(
|
|
125
|
+
registry: CustomElementRegistry,
|
|
126
|
+
config: MountConfig,
|
|
127
|
+
registryRoot: Node
|
|
128
|
+
): Promise<ObserverEntry> {
|
|
129
|
+
// Add config to the registry's config list (if not already there)
|
|
130
|
+
(registry as any).mountConfigRegistry.push(config);
|
|
131
|
+
|
|
132
|
+
// Get or create the nested map structure
|
|
133
|
+
const mountConfigMap = (getRegistryObservers()).getOrInsertComputed(registry, () => new Map());
|
|
134
|
+
const nodeToObserverMap = mountConfigMap.getOrInsertComputed(config, () => new WeakMap());
|
|
135
|
+
|
|
136
|
+
// Get or create the observer for this specific registry root
|
|
137
|
+
let observerEntry = nodeToObserverMap.get(registryRoot);
|
|
138
|
+
if (!observerEntry) {
|
|
139
|
+
observerEntry = await createObserverEntry(config, registryRoot);
|
|
140
|
+
nodeToObserverMap.set(registryRoot, observerEntry);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Track this registry root in the scopes set
|
|
144
|
+
const scopes = getRegistryScopes().getOrInsertComputed(registry, () => ({
|
|
145
|
+
weakSet: new WeakSet<Node>(),
|
|
146
|
+
setWeak: new Set<WeakRef<Node>>()
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
// Add to tracking sets if not already present
|
|
150
|
+
if (!scopes.weakSet.has(registryRoot)) {
|
|
151
|
+
scopes.weakSet.add(registryRoot);
|
|
152
|
+
scopes.setWeak.add(new WeakRef(registryRoot));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get all configs for this registry
|
|
156
|
+
const configs = (registry as any).mountConfigRegistry.items;
|
|
157
|
+
|
|
158
|
+
// Iterate over all known registry roots for this registry
|
|
159
|
+
const arr = Array.from(scopes.setWeak) as WeakRef<Node>[];
|
|
160
|
+
for (const regRootRef of arr) {
|
|
161
|
+
const regRoot = regRootRef.deref();
|
|
162
|
+
if (regRoot === undefined) continue;
|
|
163
|
+
|
|
164
|
+
// For each config, ensure an observer exists for this registry root
|
|
165
|
+
for (const conf of configs) {
|
|
166
|
+
// Skip if this is the same config + root we just created
|
|
167
|
+
if (conf === config && registryRoot === regRoot) continue;
|
|
168
|
+
|
|
169
|
+
// Get or create observer for this conf + regRoot combination
|
|
170
|
+
// This won't cause infinite loop because we only create if missing
|
|
171
|
+
const confObserverMap = mountConfigMap.getOrInsertComputed(conf, () => new WeakMap());
|
|
172
|
+
let existingEntry = confObserverMap.get(regRoot);
|
|
173
|
+
if (!existingEntry) {
|
|
174
|
+
existingEntry = await createObserverEntry(conf, regRoot);
|
|
175
|
+
confObserverMap.set(regRoot, existingEntry);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return observerEntry;
|
|
181
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { DismountEvent } from './Events.js';
|
|
2
|
+
export function setupConnectionMonitor(init, rootNodeRef, mountedElements, modules, observer, processNode) {
|
|
3
|
+
const { whereConnectionHas } = init;
|
|
4
|
+
if (!whereConnectionHas) {
|
|
5
|
+
throw new Error('whereConnectionHas is required');
|
|
6
|
+
}
|
|
7
|
+
// Get connection object with vendor prefixes
|
|
8
|
+
const nav = navigator;
|
|
9
|
+
const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
10
|
+
// If Network Information API is not supported, warn and pass the condition
|
|
11
|
+
if (!connection) {
|
|
12
|
+
console.warn('Network Information API is not supported in this browser. whereConnectionHas condition will be ignored.');
|
|
13
|
+
return {
|
|
14
|
+
conditionMatches: true,
|
|
15
|
+
cleanup: () => { }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Check initial condition
|
|
19
|
+
let conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
|
|
20
|
+
// Set up change listener
|
|
21
|
+
const changeHandler = () => {
|
|
22
|
+
const previousMatches = conditionMatches;
|
|
23
|
+
conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
|
|
24
|
+
if (conditionMatches && !previousMatches) {
|
|
25
|
+
// Connection now matches - process elements
|
|
26
|
+
handleConditionMatch();
|
|
27
|
+
}
|
|
28
|
+
else if (!conditionMatches && previousMatches) {
|
|
29
|
+
// Connection no longer matches - dismount all elements
|
|
30
|
+
handleConditionUnmatch();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
function handleConditionMatch() {
|
|
34
|
+
// Process all elements in the observed node
|
|
35
|
+
const rootNode = rootNodeRef.deref();
|
|
36
|
+
if (rootNode) {
|
|
37
|
+
processNode(rootNode);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function handleConditionUnmatch() {
|
|
41
|
+
// Dismount all currently mounted elements
|
|
42
|
+
const rootNode = rootNodeRef.deref();
|
|
43
|
+
if (!rootNode) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const context = {
|
|
47
|
+
modules,
|
|
48
|
+
observer: observer,
|
|
49
|
+
rootNode,
|
|
50
|
+
mountConfig: init
|
|
51
|
+
};
|
|
52
|
+
// Get all mounted elements from the WeakDual setWeak
|
|
53
|
+
const mountedElementsList = [];
|
|
54
|
+
for (const ref of mountedElements.setWeak) {
|
|
55
|
+
const element = ref.deref();
|
|
56
|
+
if (element) {
|
|
57
|
+
mountedElementsList.push(element);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Dismount each element
|
|
61
|
+
for (const element of mountedElementsList) {
|
|
62
|
+
// Remove from both structures
|
|
63
|
+
mountedElements.weakSet.delete(element);
|
|
64
|
+
for (const ref of mountedElements.setWeak) {
|
|
65
|
+
if (ref.deref() === element) {
|
|
66
|
+
mountedElements.setWeak.delete(ref);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Dispatch dismount event with reason
|
|
71
|
+
observer.dispatchEvent(new DismountEvent(element, 'connection-failed', init));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Listen for connection changes
|
|
75
|
+
connection.addEventListener('change', changeHandler);
|
|
76
|
+
return {
|
|
77
|
+
conditionMatches,
|
|
78
|
+
cleanup: () => {
|
|
79
|
+
connection.removeEventListener('change', changeHandler);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Evaluate if the current connection meets the specified conditions
|
|
85
|
+
*/
|
|
86
|
+
function evaluateConnectionCondition(connection, condition) {
|
|
87
|
+
// Check effectiveType (e.g., 'slow-2g', '2g', '3g', '4g')
|
|
88
|
+
if (condition.effectiveTypeIn && condition.effectiveTypeIn.length > 0) {
|
|
89
|
+
const effectiveType = connection.effectiveType;
|
|
90
|
+
if (!effectiveType || !condition.effectiveTypeIn.includes(effectiveType)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Check downlink (bandwidth in Mbps)
|
|
95
|
+
if (condition.downlinkMin !== undefined) {
|
|
96
|
+
const downlink = connection.downlink;
|
|
97
|
+
if (downlink === undefined || downlink < condition.downlinkMin) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (condition.downlinkMax !== undefined) {
|
|
102
|
+
const downlink = connection.downlink;
|
|
103
|
+
if (downlink === undefined || downlink > condition.downlinkMax) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check RTT (round-trip time in ms)
|
|
108
|
+
if (condition.rttMax !== undefined) {
|
|
109
|
+
const rtt = connection.rtt;
|
|
110
|
+
if (rtt === undefined || rtt > condition.rttMax) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// All conditions passed
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Network connection monitoring for MountObserver
|
|
2
|
+
import type { MountConfig, MountContext, WeakDual, ConnectionCondition } from './types/mount-observer/types.js';
|
|
3
|
+
import { DismountEvent } from './Events.js';
|
|
4
|
+
|
|
5
|
+
// Extend Navigator type to include connection properties
|
|
6
|
+
interface NetworkInformation extends EventTarget {
|
|
7
|
+
downlink?: number;
|
|
8
|
+
effectiveType?: string;
|
|
9
|
+
rtt?: number;
|
|
10
|
+
saveData?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NavigatorWithConnection extends Navigator {
|
|
14
|
+
connection?: NetworkInformation;
|
|
15
|
+
mozConnection?: NetworkInformation;
|
|
16
|
+
webkitConnection?: NetworkInformation;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setupConnectionMonitor(
|
|
20
|
+
init: MountConfig,
|
|
21
|
+
rootNodeRef: WeakRef<Node>,
|
|
22
|
+
mountedElements: WeakDual<Element>,
|
|
23
|
+
modules: any[],
|
|
24
|
+
observer: EventTarget,
|
|
25
|
+
processNode: (node: Node) => void
|
|
26
|
+
): {
|
|
27
|
+
conditionMatches: boolean;
|
|
28
|
+
cleanup: () => void;
|
|
29
|
+
} {
|
|
30
|
+
const { whereConnectionHas } = init;
|
|
31
|
+
|
|
32
|
+
if (!whereConnectionHas) {
|
|
33
|
+
throw new Error('whereConnectionHas is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get connection object with vendor prefixes
|
|
37
|
+
const nav = navigator as NavigatorWithConnection;
|
|
38
|
+
const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
39
|
+
|
|
40
|
+
// If Network Information API is not supported, warn and pass the condition
|
|
41
|
+
if (!connection) {
|
|
42
|
+
console.warn('Network Information API is not supported in this browser. whereConnectionHas condition will be ignored.');
|
|
43
|
+
return {
|
|
44
|
+
conditionMatches: true,
|
|
45
|
+
cleanup: () => {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check initial condition
|
|
50
|
+
let conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
|
|
51
|
+
|
|
52
|
+
// Set up change listener
|
|
53
|
+
const changeHandler = () => {
|
|
54
|
+
const previousMatches = conditionMatches;
|
|
55
|
+
conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
|
|
56
|
+
|
|
57
|
+
if (conditionMatches && !previousMatches) {
|
|
58
|
+
// Connection now matches - process elements
|
|
59
|
+
handleConditionMatch();
|
|
60
|
+
} else if (!conditionMatches && previousMatches) {
|
|
61
|
+
// Connection no longer matches - dismount all elements
|
|
62
|
+
handleConditionUnmatch();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function handleConditionMatch(): void {
|
|
67
|
+
// Process all elements in the observed node
|
|
68
|
+
const rootNode = rootNodeRef.deref();
|
|
69
|
+
if (rootNode) {
|
|
70
|
+
processNode(rootNode);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleConditionUnmatch(): void {
|
|
75
|
+
// Dismount all currently mounted elements
|
|
76
|
+
const rootNode = rootNodeRef.deref();
|
|
77
|
+
if (!rootNode) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const context: MountContext = {
|
|
82
|
+
modules,
|
|
83
|
+
observer: observer as any,
|
|
84
|
+
rootNode,
|
|
85
|
+
mountConfig: init
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Get all mounted elements from the WeakDual setWeak
|
|
89
|
+
const mountedElementsList: Element[] = [];
|
|
90
|
+
for (const ref of mountedElements.setWeak) {
|
|
91
|
+
const element = ref.deref();
|
|
92
|
+
if (element) {
|
|
93
|
+
mountedElementsList.push(element);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Dismount each element
|
|
98
|
+
for (const element of mountedElementsList) {
|
|
99
|
+
// Remove from both structures
|
|
100
|
+
mountedElements.weakSet.delete(element);
|
|
101
|
+
for (const ref of mountedElements.setWeak) {
|
|
102
|
+
if (ref.deref() === element) {
|
|
103
|
+
mountedElements.setWeak.delete(ref);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Dispatch dismount event with reason
|
|
109
|
+
observer.dispatchEvent(new DismountEvent(element, 'connection-failed', init));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Listen for connection changes
|
|
114
|
+
connection.addEventListener('change', changeHandler);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
conditionMatches,
|
|
118
|
+
cleanup: () => {
|
|
119
|
+
connection.removeEventListener('change', changeHandler);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Evaluate if the current connection meets the specified conditions
|
|
126
|
+
*/
|
|
127
|
+
function evaluateConnectionCondition(
|
|
128
|
+
connection: NetworkInformation,
|
|
129
|
+
condition: ConnectionCondition
|
|
130
|
+
): boolean {
|
|
131
|
+
// Check effectiveType (e.g., 'slow-2g', '2g', '3g', '4g')
|
|
132
|
+
if (condition.effectiveTypeIn && condition.effectiveTypeIn.length > 0) {
|
|
133
|
+
const effectiveType = connection.effectiveType;
|
|
134
|
+
if (!effectiveType || !condition.effectiveTypeIn.includes(effectiveType)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check downlink (bandwidth in Mbps)
|
|
140
|
+
if (condition.downlinkMin !== undefined) {
|
|
141
|
+
const downlink = connection.downlink;
|
|
142
|
+
if (downlink === undefined || downlink < condition.downlinkMin) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (condition.downlinkMax !== undefined) {
|
|
148
|
+
const downlink = connection.downlink;
|
|
149
|
+
if (downlink === undefined || downlink > condition.downlinkMax) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check RTT (round-trip time in ms)
|
|
155
|
+
if (condition.rttMax !== undefined) {
|
|
156
|
+
const rtt = connection.rtt;
|
|
157
|
+
if (rtt === undefined || rtt > condition.rttMax) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// All conditions passed
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DismountEvent } from './Events.js';
|
|
2
|
+
export function setupElementIntersection(init, rootNodeRef, mountedElements, modules, observer, matchesSelector, handleMatch) {
|
|
3
|
+
const { whereElementIntersectsWith } = init;
|
|
4
|
+
if (!whereElementIntersectsWith) {
|
|
5
|
+
throw new Error('whereElementIntersectsWith is required');
|
|
6
|
+
}
|
|
7
|
+
// Track which elements are currently intersecting
|
|
8
|
+
const intersectingElements = new WeakSet();
|
|
9
|
+
// Create IntersectionObserver with the provided options
|
|
10
|
+
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const element = entry.target;
|
|
13
|
+
if (entry.isIntersecting) {
|
|
14
|
+
// Element is now intersecting
|
|
15
|
+
intersectingElements.add(element);
|
|
16
|
+
// Check if element matches all other conditions and mount if so
|
|
17
|
+
if (matchesSelector(element)) {
|
|
18
|
+
handleMatch(element);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Element is no longer intersecting
|
|
23
|
+
intersectingElements.delete(element);
|
|
24
|
+
// Dismount if it was mounted
|
|
25
|
+
if (mountedElements.weakSet.has(element)) {
|
|
26
|
+
dismountElement(element);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}, whereElementIntersectsWith);
|
|
31
|
+
function dismountElement(element) {
|
|
32
|
+
// Remove from mounted elements
|
|
33
|
+
mountedElements.weakSet.delete(element);
|
|
34
|
+
for (const ref of mountedElements.setWeak) {
|
|
35
|
+
if (ref.deref() === element) {
|
|
36
|
+
mountedElements.setWeak.delete(ref);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Dispatch dismount event
|
|
41
|
+
observer.dispatchEvent(new DismountEvent(element, 'intersection-failed', init));
|
|
42
|
+
}
|
|
43
|
+
function observeElement(element) {
|
|
44
|
+
intersectionObserver.observe(element);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
intersectionObserver,
|
|
48
|
+
observeElement,
|
|
49
|
+
cleanup: () => {
|
|
50
|
+
intersectionObserver.disconnect();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if an element is currently intersecting
|
|
56
|
+
* This is called from #matchesSelector to determine if intersection condition is met
|
|
57
|
+
*/
|
|
58
|
+
export function isElementIntersecting(element, intersectionObserver) {
|
|
59
|
+
// If no intersection observer is set up, consider all elements as intersecting
|
|
60
|
+
if (!intersectionObserver) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// When intersection observer is active, we can't synchronously determine intersection state
|
|
64
|
+
// The element will be observed and the callback will handle mounting when it intersects
|
|
65
|
+
// Return false here to prevent immediate mounting
|
|
66
|
+
return false;
|
|
67
|
+
}
|