Repository: LeaVerou/style-observer
Pull request #117: Remove side effects experiment
Author: login: LeaVerou
Created: 2025-05-21T12:11:02Z | Merged: 2025-05-21T14:33:24Z



Diff:
diff --git a/src/element-style-observer.js b/src/element-style-observer.js
index 4be431c..29e2add 100644
--- a/src/element-style-observer.js
+++ b/src/element-style-observer.js
@@ -5,13 +5,14 @@ import { toArray, wait, getTimesFor } from "./util.js";
 import RenderedObserver from "./rendered-observer.js";
 
 // Start bug detection early if we have a DOM
+let allowDiscrete = "";
 if (globalThis.document) {
-	Bugs.detect();
-}
+	Bugs.detectAll();
 
-const allowDiscrete = globalThis.CSS?.supports?.("transition-behavior", "allow-discrete")
-	? " allow-discrete"
-	: "";
+	allowDiscrete = globalThis.CSS?.supports?.("transition-behavior", "allow-discrete")
+		? " allow-discrete"
+		: "";
+}
 
 /**
  * @typedef { object } StyleObserverOptionsObject
@@ -82,7 +83,7 @@ export default class ElementStyleObserver {
 	constructor (target, callback, options = {}) {
 		gentleRegisterProperty("--style-observer-transition", { inherits: false });
 
-		Bugs.detect();
+		Bugs.detectAll();
 
 		this.constructor.all.add(target, this);
 		this.properties = new Map();
@@ -105,7 +106,7 @@ export default class ElementStyleObserver {
 	/**
 	 * Called the first time observe() is called to initialize the target.
 	 */
-	#init () {
+	#init() {
 		if (this.#initialized) {
 			return;
 		}
@@ -131,13 +132,13 @@ export default class ElementStyleObserver {
 		}
 
 		if (
-			(Bugs.transitionRunLoop && event?.type === "transitionrun") ||
+			(Bugs.TRANSITIONRUN_EVENT_LOOP_BUG && event?.type === "transitionrun") ||
 			this.options.throttle > 0
 		) {
-			let eventName = Bugs.transitionRunLoop ? "transitionrun" : "transitionstart";
+			let eventName = Bugs.TRANSITIONRUN_EVENT_LOOP_BUG ? "transitionrun" : "transitionstart";
 			let delay = Math.max(this.options.throttle, 50);
 
-			if (Bugs.transitionRunLoop) {
+			if (Bugs.TRANSITIONRUN_EVENT_LOOP_BUG) {
 				// Safari < 18.2 fires `transitionrun` events too often, so we need to debounce.
 				// Wait at least the amount of time needed for the transition to run + 1 frame (~16ms)
 				let times = getTimesFor(
@@ -192,7 +193,7 @@ export default class ElementStyleObserver {
 		let cs = getComputedStyle(this.target);
 
 		for (let property of properties) {
-			if (Bugs.unregisteredTransition && !this.constructor.properties.has(property)) {
+			if (Bugs.UNREGISTERED_TRANSITION_BUG && !this.constructor.properties.has(property)) {
 				// Init property
 				gentleRegisterProperty(property, undefined, this.target.ownerDocument.defaultView);
 				this.constructor.properties.add(property);
@@ -202,7 +203,7 @@ export default class ElementStyleObserver {
 			this.properties.set(property, value);
 		}
 
-		if (Bugs.transitionRunLoop) {
+		if (Bugs.TRANSITIONRUN_EVENT_LOOP_BUG) {
 			this.target.addEventListener("transitionrun", this);
 		}
 
diff --git a/src/util/detect-bugs.js b/src/util/detect-bugs.js
index 2c7ca00..35aa194 100644
--- a/src/util/detect-bugs.js
+++ b/src/util/detect-bugs.js
@@ -1,63 +1,32 @@
-import detectTransitionRunLoopBug from "./detect-bugs/transitionrun-loop.js";
-import detectUnregisteredTransitionBug from "./detect-bugs/unregistered-transition.js";
+import TRANSITIONRUN_EVENT_LOOP_BUG from "./detect-bugs/transitionrun-loop.js";
+import UNREGISTERED_TRANSITION_BUG from "./detect-bugs/unregistered-transition.js";
 
-export default class Bugs {
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean | null}
-	 */
-	static #transitionRunLoop = null;
+export const detectors = { TRANSITIONRUN_EVENT_LOOP_BUG, UNREGISTERED_TRANSITION_BUG };
+/**
+ * Data structure for all detected bugs.
+ * All bugs start off as true, and once their promises resolve, that is replaced with the actual value
+ */
+export const bugs = {
+	detectAll () {
+		return Promise.all(Object.values(detectors).map(detector => detector.valuePending));
+	},
+};
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean | null}
-	 */
-	static #unregisteredTransition = null;
-
-	/**
-	 * Detect all bugs in parallel
-	 * @returns {Promise<{ transitionRunLoop: boolean, unregisteredTransition: boolean }>}
-	 */
-	static async detect () {
-		const [transitionRunLoop, unregisteredTransition] = await Promise.all([
-			detectTransitionRunLoopBug(),
-			detectUnregisteredTransitionBug(),
-		]);
-
-		this.#transitionRunLoop = transitionRunLoop;
-		this.#unregisteredTransition = unregisteredTransition;
-
-		return {
-			transitionRunLoop,
-			unregisteredTransition,
-		};
-	}
-
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean}
-	 */
-	static get transitionRunLoop () {
-		if (this.#transitionRunLoop === null) {
-			this.#transitionRunLoop = true; // Default to true while detecting, just in case
-			detectTransitionRunLoopBug().then(result => {
-				this.#transitionRunLoop = result;
+for (let bug in detectors) {
+	let detector = detectors[bug];
+	Object.defineProperty(bugs, bug, {
+		get () {
+			detector.valuePending.then(value => {
+				delete this[bug];
+				this[bug] = value;
 			});
-		}
-		return this.#transitionRunLoop;
-	}
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean}
-	 */
-	static get unregisteredTransition () {
-		if (this.#unregisteredTransition === null) {
-			this.#unregisteredTransition = true; // Default to true while detecting, just in case
-			detectUnregisteredTransitionBug().then(result => {
-				this.#unregisteredTransition = result;
-			});
-		}
-		return this.#unregisteredTransition;
-	}
+			// Until we know, assume the bug is present
+			return true;
+		},
+		configurable: true,
+		enumerable: true,
+	});
 }
+
+export default bugs;
diff --git a/src/util/detect-bugs/transitionrun-loop.js b/src/util/detect-bugs/transitionrun-loop.js
index 05ab9e5..77eef5f 100644
--- a/src/util/detect-bugs/transitionrun-loop.js
+++ b/src/util/detect-bugs/transitionrun-loop.js
@@ -2,31 +2,33 @@
 // causes an infinite loop of `transitionrun` or `transitionstart` events.
 // We use this test to detect the bug.
 
-let result = null;
-
 /**
  * Detect if the browser is affected by the Safari transition loop bug.
  * @see https://bugs.webkit.org/show_bug.cgi?id=279012
  * @returns {Promise<boolean>}
  */
-export default function detectTransitionRunLoopBug () {
-	if (result !== null) {
-		return result;
-	}
-
-	let dummy = document.createElement("div");
-	document.body.appendChild(dummy);
-	let property = "--bar-" + Date.now();
-	dummy.style.cssText = `${property}: 1; transition: ${property} 1ms step-start allow-discrete`;
+export default {
+	value: undefined,
+	get valuePending () {
+		if (this.value !== undefined) {
+			return this.value;
+		}
 
-	result = new Promise(resolve => {
-		let eventsCount = 0;
-		requestAnimationFrame(() => {
-			setTimeout(_ => resolve(eventsCount > 1), 50);
-			dummy.addEventListener("transitionrun", _ => eventsCount++);
-			dummy.style.setProperty(property, "2");
-		});
-	}).finally(() => dummy.remove());
+		let dummy = document.createElement("div");
+		document.body.appendChild(dummy);
+		let property = "--bar-" + Date.now();
+		dummy.style.cssText = `${property}: 1; transition: ${property} 1ms step-start allow-discrete`;
 
-	return result;
-}
+		delete this.valuePending;
+		return (this.valuePending = new Promise(resolve => {
+			let eventsCount = 0;
+			requestAnimationFrame(() => {
+				setTimeout(_ => resolve(eventsCount > 1), 50);
+				dummy.addEventListener("transitionrun", _ => eventsCount++);
+				dummy.style.setProperty(property, "2");
+			});
+		})
+			.then(v => (this.value = v))
+			.finally(() => dummy.remove()));
+	},
+};
diff --git a/src/util/detect-bugs/unregistered-transition.js b/src/util/detect-bugs/unregistered-transition.js
index 475cce0..be53d88 100644
--- a/src/util/detect-bugs/unregistered-transition.js
+++ b/src/util/detect-bugs/unregistered-transition.js
@@ -1,29 +1,31 @@
 // Detect https://issues.chromium.org/issues/360159391
 
-let result = null;
-
 /**
  * Detect if the browser is affected by the unregistered transition bug.
  * @see https://issues.chromium.org/issues/360159391
  * @returns {Promise<boolean>}
  */
-export default function detectUnregisteredTransitionBug () {
-	if (result !== null) {
-		return result;
-	}
-
-	let dummy = document.createElement("div");
-	document.body.appendChild(dummy);
-	let property = "--foo-" + Date.now();
-	dummy.style.cssText = `${property}: 1; transition: ${property} 1ms step-start allow-discrete`;
+export default {
+	value: undefined,
+	get valuePending () {
+		if (this.value !== undefined) {
+			return this.value;
+		}
 
-	result = new Promise(resolve => {
-		requestAnimationFrame(() => {
-			setTimeout(_ => resolve(true), 30);
-			dummy.addEventListener("transitionstart", _ => resolve(false));
-			dummy.style.setProperty(property, "2");
-		});
-	}).finally(() => dummy.remove());
+		let dummy = document.createElement("div");
+		document.body.appendChild(dummy);
+		let property = "--foo-" + Date.now();
+		dummy.style.cssText = `${property}: 1; transition: ${property} 1ms step-start allow-discrete`;
 
-	return result;
-}
+		delete this.valuePending;
+		return (this.valuePending = new Promise(resolve => {
+			requestAnimationFrame(() => {
+				setTimeout(_ => resolve(true), 30);
+				dummy.addEventListener("transitionstart", _ => resolve(false));
+				dummy.style.setProperty(property, "2");
+			});
+		})
+			.then(v => (this.value = v))
+			.finally(() => dummy.remove()));
+	},
+};


Reviews:
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T12:21:56Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T12:54:16Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T12:59:33Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T13:00:17Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T13:00:38Z
user: login: benface
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T13:39:54Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T13:43:35Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T13:43:58Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T13:44:18Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T13:44:32Z
user: login: LeaVerou
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T14:13:37Z
user: login: DmitrySharabin
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-21T14:16:25Z
user: login: benface
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T14:31:10Z
user: login: benface
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T14:33:04Z
user: login: benface
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T14:34:21Z
user: login: DmitrySharabin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T14:36:56Z
user: login: benface
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T21:35:23Z
user: login: LeaVerou
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-22T14:52:43Z
user: login: benface
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T15:04:31Z

Review comments:
diff_hunk: @@ -1,63 +1,33 @@
-import detectTransitionRunLoopBug from "./detect-bugs/transitionrun-loop.js";
-import detectUnregisteredTransitionBug from "./detect-bugs/unregistered-transition.js";
+import TRANSITIONRUN_EVENT_LOOP_BUG from "./detect-bugs/transitionrun-loop.js";
+import UNREGISTERED_TRANSITION_BUG from "./detect-bugs/unregistered-transition.js";
 
-export default class Bugs {
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean | null}
-	 */
-	static #transitionRunLoop = null;
+export const detectors = { TRANSITIONRUN_EVENT_LOOP_BUG, UNREGISTERED_TRANSITION_BUG };
+/**
+ * Data structure for all detected bugs.
+ * All bugs start off as true, and once their promises resolve, that is replaced with the actual value
+ */
+export const bugs = {
+	detectAll () {
+		return Promise.all(Object.values(detectors).map(detector => detector.valuePending));
+	},
+};
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean | null}
-	 */
-	static #unregisteredTransition = null;
-
-	/**
-	 * Detect all bugs in parallel
-	 * @returns {Promise<{ transitionRunLoop: boolean, unregisteredTransition: boolean }>}
-	 */
-	static async detect () {
-		const [transitionRunLoop, unregisteredTransition] = await Promise.all([
-			detectTransitionRunLoopBug(),
-			detectUnregisteredTransitionBug(),
-		]);
-
-		this.#transitionRunLoop = transitionRunLoop;
-		this.#unregisteredTransition = unregisteredTransition;
-
-		return {
-			transitionRunLoop,
-			unregisteredTransition,
-		};
-	}
-
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean}
-	 */
-	static get transitionRunLoop () {
-		if (this.#transitionRunLoop === null) {
-			this.#transitionRunLoop = true; // Default to true while detecting, just in case
-			detectTransitionRunLoopBug().then(result => {
-				this.#transitionRunLoop = result;
+for (let bug in detectors) {
+	let detector = bugs[bug];
+	Object.defineProperty(bugs, bug, {
+		get () {
+			detector.valuePending.then(value => {
+				delete this[bug];
+				this[bug] = value;
 			});
-		}
-		return this.#transitionRunLoop;
-	}
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean}
-	 */
-	static get unregisteredTransition () {
-		if (this.#unregisteredTransition === null) {
-			this.#unregisteredTransition = true; // Default to true while detecting, just in case
-			detectUnregisteredTransitionBug().then(result => {
-				this.#unregisteredTransition = result;
-			});
-		}
-		return this.#unregisteredTransition;
-	}
+			// Until we know, assume the bug is present
+			return true;
+		},
+		configurable: true,
+		enumerable: true,
+		writable: true,
path: src/util/detect-bugs.js
user: login: DmitrySharabin
body: This seems to make the property descriptor invalid—we can't have this when we have accessors. Right?
created_at: 2025-05-21T12:21:56Z
author_association: COLLABORATOR
diff_hunk: @@ -1,63 +1,33 @@
-import detectTransitionRunLoopBug from "./detect-bugs/transitionrun-loop.js";
-import detectUnregisteredTransitionBug from "./detect-bugs/unregistered-transition.js";
+import TRANSITIONRUN_EVENT_LOOP_BUG from "./detect-bugs/transitionrun-loop.js";
+import UNREGISTERED_TRANSITION_BUG from "./detect-bugs/unregistered-transition.js";
 
-export default class Bugs {
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean | null}
-	 */
-	static #transitionRunLoop = null;
+export const detectors = { TRANSITIONRUN_EVENT_LOOP_BUG, UNREGISTERED_TRANSITION_BUG };
+/**
+ * Data structure for all detected bugs.
+ * All bugs start off as true, and once their promises resolve, that is replaced with the actual value
+ */
+export const bugs = {
+	detectAll () {
+		return Promise.all(Object.values(detectors).map(detector => detector.valuePending));
+	},
+};
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean | null}
-	 */
-	static #unregisteredTransition = null;
-
-	/**
-	 * Detect all bugs in parallel
-	 * @returns {Promise<{ transitionRunLoop: boolean, unregisteredTransition: boolean }>}
-	 */
-	static async detect () {
-		const [transitionRunLoop, unregisteredTransition] = await Promise.all([
-			detectTransitionRunLoopBug(),
-			detectUnregisteredTransitionBug(),
-		]);
-
-		this.#transitionRunLoop = transitionRunLoop;
-		this.#unregisteredTransition = unregisteredTransition;
-
-		return {
-			transitionRunLoop,
-			unregisteredTransition,
-		};
-	}
-
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean}
-	 */
-	static get transitionRunLoop () {
-		if (this.#transitionRunLoop === null) {
-			this.#transitionRunLoop = true; // Default to true while detecting, just in case
-			detectTransitionRunLoopBug().then(result => {
-				this.#transitionRunLoop = result;
+for (let bug in detectors) {
+	let detector = bugs[bug];
path: src/util/detect-bugs.js
user: login: DmitrySharabin
body: ```suggestion
	let detector = detectors[bug];
```
created_at: 2025-05-21T12:54:16Z
author_association: COLLABORATOR
diff_hunk: @@ -105,7 +106,7 @@ export default class ElementStyleObserver {
 	/**
 	 * Called the first time observe() is called to initialize the target.
 	 */
-	#init () {
+	#init() {
path: src/element-style-observer.js
user: login: DmitrySharabin
body: We should update our Prettier plugin: we fixed the issue with private methods in `0.0.8`.

**EDIT:** It's up-to-date in the project
created_at: 2025-05-21T12:59:33Z
author_association: COLLABORATOR
diff_hunk: @@ -105,7 +106,7 @@ export default class ElementStyleObserver {
 	/**
 	 * Called the first time observe() is called to initialize the target.
 	 */
-	#init () {
+	#init() {
path: src/element-style-observer.js
user: login: DmitrySharabin
body: You probably still have the outdated one in your local setup
created_at: 2025-05-21T13:00:17Z
author_association: COLLABORATOR
diff_hunk: @@ -1,63 +1,33 @@
-import detectTransitionRunLoopBug from "./detect-bugs/transitionrun-loop.js";
-import detectUnregisteredTransitionBug from "./detect-bugs/unregistered-transition.js";
+import TRANSITIONRUN_EVENT_LOOP_BUG from "./detect-bugs/transitionrun-loop.js";
+import UNREGISTERED_TRANSITION_BUG from "./detect-bugs/unregistered-transition.js";
 
-export default class Bugs {
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean | null}
-	 */
-	static #transitionRunLoop = null;
+export const detectors = { TRANSITIONRUN_EVENT_LOOP_BUG, UNREGISTERED_TRANSITION_BUG };
+/**
+ * Data structure for all detected bugs.
+ * All bugs start off as true, and once their promises resolve, that is replaced with the actual value
+ */
+export const bugs = {
+	detectAll () {
+		return Promise.all(Object.values(detectors).map(detector => detector.valuePending));
+	},
+};
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean | null}
-	 */
-	static #unregisteredTransition = null;
-
-	/**
-	 * Detect all bugs in parallel
-	 * @returns {Promise<{ transitionRunLoop: boolean, unregisteredTransition: boolean }>}
-	 */
-	static async detect () {
-		const [transitionRunLoop, unregisteredTransition] = await Promise.all([
-			detectTransitionRunLoopBug(),
-			detectUnregisteredTransitionBug(),
-		]);
-
-		this.#transitionRunLoop = transitionRunLoop;
-		this.#unregisteredTransition = unregisteredTransition;
-
-		return {
-			transitionRunLoop,
-			unregisteredTransition,
-		};
-	}
-
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean}
-	 */
-	static get transitionRunLoop () {
-		if (this.#transitionRunLoop === null) {
-			this.#transitionRunLoop = true; // Default to true while detecting, just in case
-			detectTransitionRunLoopBug().then(result => {
-				this.#transitionRunLoop = result;
+for (let bug in detectors) {
+	let detector = bugs[bug];
+	Object.defineProperty(bugs, bug, {
+		get () {
+			detector.valuePending.then(value => {
+				delete this[bug];
+				this[bug] = value;
 			});
-		}
-		return this.#transitionRunLoop;
-	}
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean}
-	 */
-	static get unregisteredTransition () {
-		if (this.#unregisteredTransition === null) {
-			this.#unregisteredTransition = true; // Default to true while detecting, just in case
-			detectUnregisteredTransitionBug().then(result => {
-				this.#unregisteredTransition = result;
-			});
-		}
-		return this.#unregisteredTransition;
-	}
+			// Until we know, assume the bug is present
+			return true;
+		},
+		configurable: true,
+		enumerable: true,
+		writable: true,
path: src/util/detect-bugs.js
user: login: DmitrySharabin
body: ```suggestion
```
created_at: 2025-05-21T13:00:38Z
author_association: COLLABORATOR
diff_hunk: @@ -5,13 +5,14 @@ import { toArray, wait, getTimesFor } from "./util.js";
 import RenderedObserver from "./rendered-observer.js";
 
 // Start bug detection early if we have a DOM
+let allowDiscrete = "";
 if (globalThis.document) {
-	Bugs.detect();
-}
+	Bugs.detectAll();
path: src/element-style-observer.js
user: login: benface
body: Is this intentionally imported as `Bugs` instead of `bugs`?
created_at: 2025-05-21T13:39:54Z
author_association: COLLABORATOR
diff_hunk: @@ -131,13 +132,13 @@ export default class ElementStyleObserver {
 		}
 
 		if (
-			(Bugs.transitionRunLoop && event?.type === "transitionrun") ||
+			(Bugs.TRANSITION_RUN_LOOP_BUG && event?.type === "transitionrun") ||
path: src/element-style-observer.js
user: login: DmitrySharabin
body: ```suggestion
			(Bugs.TRANSITIONRUN_EVENT_LOOP_BUG && event?.type === "transitionrun") ||
```
created_at: 2025-05-21T13:43:34Z
author_association: COLLABORATOR
diff_hunk: @@ -131,13 +132,13 @@ export default class ElementStyleObserver {
 		}
 
 		if (
-			(Bugs.transitionRunLoop && event?.type === "transitionrun") ||
+			(Bugs.TRANSITION_RUN_LOOP_BUG && event?.type === "transitionrun") ||
 			this.options.throttle > 0
 		) {
-			let eventName = Bugs.transitionRunLoop ? "transitionrun" : "transitionstart";
+			let eventName = Bugs.TRANSITION_RUN_LOOP_BUG ? "transitionrun" : "transitionstart";
path: src/element-style-observer.js
user: login: DmitrySharabin
body: ```suggestion
			let eventName = Bugs.TRANSITIONRUN_EVENT_LOOP_BUG ? "transitionrun" : "transitionstart";
```
created_at: 2025-05-21T13:43:58Z
author_association: COLLABORATOR
diff_hunk: @@ -131,13 +132,13 @@ export default class ElementStyleObserver {
 		}
 
 		if (
-			(Bugs.transitionRunLoop && event?.type === "transitionrun") ||
+			(Bugs.TRANSITION_RUN_LOOP_BUG && event?.type === "transitionrun") ||
 			this.options.throttle > 0
 		) {
-			let eventName = Bugs.transitionRunLoop ? "transitionrun" : "transitionstart";
+			let eventName = Bugs.TRANSITION_RUN_LOOP_BUG ? "transitionrun" : "transitionstart";
 			let delay = Math.max(this.options.throttle, 50);
 
-			if (Bugs.transitionRunLoop) {
+			if (Bugs.TRANSITION_RUN_LOOP_BUG) {
path: src/element-style-observer.js
user: login: DmitrySharabin
body: ```suggestion
			if (Bugs.TRANSITIONRUN_EVENT_LOOP_BUG) {
```
created_at: 2025-05-21T13:44:18Z
author_association: COLLABORATOR
diff_hunk: @@ -202,7 +203,7 @@ export default class ElementStyleObserver {
 			this.properties.set(property, value);
 		}
 
-		if (Bugs.transitionRunLoop) {
+		if (Bugs.TRANSITION_RUN_LOOP_BUG) {
path: src/element-style-observer.js
user: login: DmitrySharabin
body: ```suggestion
		if (Bugs.TRANSITIONRUN_EVENT_LOOP_BUG) {
```
created_at: 2025-05-21T13:44:31Z
author_association: COLLABORATOR
diff_hunk: @@ -5,13 +5,14 @@ import { toArray, wait, getTimesFor } from "./util.js";
 import RenderedObserver from "./rendered-observer.js";
 
 // Start bug detection early if we have a DOM
+let allowDiscrete = "";
 if (globalThis.document) {
-	Bugs.detect();
-}
+	Bugs.detectAll();
path: src/element-style-observer.js
user: login: LeaVerou
body: I was trying to minimize changes, and since it's a default import it doesn't matter. But `bugs` would probably be better indeed!
created_at: 2025-05-21T14:13:37Z
author_association: OWNER
diff_hunk: @@ -1,63 +1,32 @@
-import detectTransitionRunLoopBug from "./detect-bugs/transitionrun-loop.js";
-import detectUnregisteredTransitionBug from "./detect-bugs/unregistered-transition.js";
+import TRANSITIONRUN_EVENT_LOOP_BUG from "./detect-bugs/transitionrun-loop.js";
+import UNREGISTERED_TRANSITION_BUG from "./detect-bugs/unregistered-transition.js";
 
-export default class Bugs {
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean | null}
-	 */
-	static #transitionRunLoop = null;
+export const detectors = { TRANSITIONRUN_EVENT_LOOP_BUG, UNREGISTERED_TRANSITION_BUG };
+/**
+ * Data structure for all detected bugs.
+ * All bugs start off as true, and once their promises resolve, that is replaced with the actual value
+ */
+export const bugs = {
+	detectAll () {
+		return Promise.all(Object.values(detectors).map(detector => detector.valuePending));
+	},
+};
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean | null}
-	 */
-	static #unregisteredTransition = null;
-
-	/**
-	 * Detect all bugs in parallel
-	 * @returns {Promise<{ transitionRunLoop: boolean, unregisteredTransition: boolean }>}
-	 */
-	static async detect () {
-		const [transitionRunLoop, unregisteredTransition] = await Promise.all([
-			detectTransitionRunLoopBug(),
-			detectUnregisteredTransitionBug(),
-		]);
-
-		this.#transitionRunLoop = transitionRunLoop;
-		this.#unregisteredTransition = unregisteredTransition;
-
-		return {
-			transitionRunLoop,
-			unregisteredTransition,
-		};
-	}
-
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean}
-	 */
-	static get transitionRunLoop () {
-		if (this.#transitionRunLoop === null) {
-			this.#transitionRunLoop = true; // Default to true while detecting, just in case
-			detectTransitionRunLoopBug().then(result => {
-				this.#transitionRunLoop = result;
+for (let bug in detectors) {
+	let detector = detectors[bug];
+	Object.defineProperty(bugs, bug, {
+		get () {
+			detector.valuePending.then(value => {
+				delete this[bug];
+				this[bug] = value;
 			});
-		}
-		return this.#transitionRunLoop;
-	}
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean}
-	 */
-	static get unregisteredTransition () {
-		if (this.#unregisteredTransition === null) {
-			this.#unregisteredTransition = true; // Default to true while detecting, just in case
-			detectUnregisteredTransitionBug().then(result => {
-				this.#unregisteredTransition = result;
-			});
-		}
-		return this.#unregisteredTransition;
-	}
+			// Until we know, assume the bug is present
+			return true;
path: src/util/detect-bugs.js
user: login: benface
body: I am most likely missing something, but will this not make every getter always return true, regardless of the actual detector's `value`? I don't see a return before this one.
created_at: 2025-05-21T14:31:09Z
author_association: COLLABORATOR
diff_hunk: @@ -2,31 +2,33 @@
 // causes an infinite loop of `transitionrun` or `transitionstart` events.
 // We use this test to detect the bug.
 
-let result = null;
-
 /**
  * Detect if the browser is affected by the Safari transition loop bug.
  * @see https://bugs.webkit.org/show_bug.cgi?id=279012
  * @returns {Promise<boolean>}
  */
-export default function detectTransitionRunLoopBug () {
-	if (result !== null) {
-		return result;
-	}
-
-	let dummy = document.createElement("div");
-	document.body.appendChild(dummy);
-	let property = "--bar-" + Date.now();
-	dummy.style.cssText = `${property}: 1; transition: ${property} 1ms step-start allow-discrete`;
+export default {
+	value: undefined,
+	get valuePending () {
+		if (this.value !== undefined) {
+			return this.value;
+		}
 
-	result = new Promise(resolve => {
-		let eventsCount = 0;
-		requestAnimationFrame(() => {
-			setTimeout(_ => resolve(eventsCount > 1), 50);
-			dummy.addEventListener("transitionrun", _ => eventsCount++);
-			dummy.style.setProperty(property, "2");
-		});
-	}).finally(() => dummy.remove());
+		let dummy = document.createElement("div");
+		document.body.appendChild(dummy);
+		let property = "--bar-" + Date.now();
+		dummy.style.cssText = `${property}: 1; transition: ${property} 1ms step-start allow-discrete`;
 
-	return result;
-}
+		delete this.valuePending;
+		return (this.valuePending = new Promise(resolve => {
path: src/util/detect-bugs/transitionrun-loop.js
user: login: benface
body: If we call the getter a second time while the initial promise is still working, this will restart the work instead of just returning the promise, no?
created_at: 2025-05-21T14:33:04Z
author_association: COLLABORATOR
diff_hunk: @@ -1,63 +1,32 @@
-import detectTransitionRunLoopBug from "./detect-bugs/transitionrun-loop.js";
-import detectUnregisteredTransitionBug from "./detect-bugs/unregistered-transition.js";
+import TRANSITIONRUN_EVENT_LOOP_BUG from "./detect-bugs/transitionrun-loop.js";
+import UNREGISTERED_TRANSITION_BUG from "./detect-bugs/unregistered-transition.js";
 
-export default class Bugs {
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean | null}
-	 */
-	static #transitionRunLoop = null;
+export const detectors = { TRANSITIONRUN_EVENT_LOOP_BUG, UNREGISTERED_TRANSITION_BUG };
+/**
+ * Data structure for all detected bugs.
+ * All bugs start off as true, and once their promises resolve, that is replaced with the actual value
+ */
+export const bugs = {
+	detectAll () {
+		return Promise.all(Object.values(detectors).map(detector => detector.valuePending));
+	},
+};
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean | null}
-	 */
-	static #unregisteredTransition = null;
-
-	/**
-	 * Detect all bugs in parallel
-	 * @returns {Promise<{ transitionRunLoop: boolean, unregisteredTransition: boolean }>}
-	 */
-	static async detect () {
-		const [transitionRunLoop, unregisteredTransition] = await Promise.all([
-			detectTransitionRunLoopBug(),
-			detectUnregisteredTransitionBug(),
-		]);
-
-		this.#transitionRunLoop = transitionRunLoop;
-		this.#unregisteredTransition = unregisteredTransition;
-
-		return {
-			transitionRunLoop,
-			unregisteredTransition,
-		};
-	}
-
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean}
-	 */
-	static get transitionRunLoop () {
-		if (this.#transitionRunLoop === null) {
-			this.#transitionRunLoop = true; // Default to true while detecting, just in case
-			detectTransitionRunLoopBug().then(result => {
-				this.#transitionRunLoop = result;
+for (let bug in detectors) {
+	let detector = detectors[bug];
+	Object.defineProperty(bugs, bug, {
+		get () {
+			detector.valuePending.then(value => {
+				delete this[bug];
+				this[bug] = value;
 			});
-		}
-		return this.#transitionRunLoop;
-	}
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean}
-	 */
-	static get unregisteredTransition () {
-		if (this.#unregisteredTransition === null) {
-			this.#unregisteredTransition = true; // Default to true while detecting, just in case
-			detectUnregisteredTransitionBug().then(result => {
-				this.#unregisteredTransition = result;
-			});
-		}
-		return this.#unregisteredTransition;
-	}
+			// Until we know, assume the bug is present
+			return true;
path: src/util/detect-bugs.js
user: login: benface
body: Oops, it's been merged. My bad for not reviewing properly before, but I _am_ likely missing something :)
created_at: 2025-05-21T14:34:20Z
author_association: COLLABORATOR
diff_hunk: @@ -1,63 +1,32 @@
-import detectTransitionRunLoopBug from "./detect-bugs/transitionrun-loop.js";
-import detectUnregisteredTransitionBug from "./detect-bugs/unregistered-transition.js";
+import TRANSITIONRUN_EVENT_LOOP_BUG from "./detect-bugs/transitionrun-loop.js";
+import UNREGISTERED_TRANSITION_BUG from "./detect-bugs/unregistered-transition.js";
 
-export default class Bugs {
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean | null}
-	 */
-	static #transitionRunLoop = null;
+export const detectors = { TRANSITIONRUN_EVENT_LOOP_BUG, UNREGISTERED_TRANSITION_BUG };
+/**
+ * Data structure for all detected bugs.
+ * All bugs start off as true, and once their promises resolve, that is replaced with the actual value
+ */
+export const bugs = {
+	detectAll () {
+		return Promise.all(Object.values(detectors).map(detector => detector.valuePending));
+	},
+};
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean | null}
-	 */
-	static #unregisteredTransition = null;
-
-	/**
-	 * Detect all bugs in parallel
-	 * @returns {Promise<{ transitionRunLoop: boolean, unregisteredTransition: boolean }>}
-	 */
-	static async detect () {
-		const [transitionRunLoop, unregisteredTransition] = await Promise.all([
-			detectTransitionRunLoopBug(),
-			detectUnregisteredTransitionBug(),
-		]);
-
-		this.#transitionRunLoop = transitionRunLoop;
-		this.#unregisteredTransition = unregisteredTransition;
-
-		return {
-			transitionRunLoop,
-			unregisteredTransition,
-		};
-	}
-
-	/**
-	 * Whether the browser has the transition run loop bug
-	 * @type {boolean}
-	 */
-	static get transitionRunLoop () {
-		if (this.#transitionRunLoop === null) {
-			this.#transitionRunLoop = true; // Default to true while detecting, just in case
-			detectTransitionRunLoopBug().then(result => {
-				this.#transitionRunLoop = result;
+for (let bug in detectors) {
+	let detector = detectors[bug];
+	Object.defineProperty(bugs, bug, {
+		get () {
+			detector.valuePending.then(value => {
+				delete this[bug];
+				this[bug] = value;
 			});
-		}
-		return this.#transitionRunLoop;
-	}
 
-	/**
-	 * Whether the browser has the unregistered transition bug
-	 * @type {boolean}
-	 */
-	static get unregisteredTransition () {
-		if (this.#unregisteredTransition === null) {
-			this.#unregisteredTransition = true; // Default to true while detecting, just in case
-			detectUnregisteredTransitionBug().then(result => {
-				this.#unregisteredTransition = result;
-			});
-		}
-		return this.#unregisteredTransition;
-	}
+			// Until we know, assume the bug is present
+			return true;
path: src/util/detect-bugs.js
user: login: DmitrySharabin
body: You still can get the answers, though. 😉
created_at: 2025-05-21T14:36:56Z
author_association: COLLABORATOR
diff_hunk: @@ -5,13 +5,14 @@ import { toArray, wait, getTimesFor } from "./util.js";
 import RenderedObserver from "./rendered-observer.js";
 
 // Start bug detection early if we have a DOM
+let allowDiscrete = "";
 if (globalThis.document) {
-	Bugs.detect();
-}
+	Bugs.detectAll();
 
-const allowDiscrete = globalThis.CSS?.supports?.("transition-behavior", "allow-discrete")
-	? " allow-discrete"
-	: "";
path: src/element-style-observer.js
user: login: benface
body: @LeaVerou – I don't understand why you moved this inside `if (globalThis.document)`; is there a case where we would have `globalThis.CSS.supports` but no `globalThis.document`?
created_at: 2025-05-21T21:35:23Z
author_association: COLLABORATOR
diff_hunk: @@ -5,13 +5,14 @@ import { toArray, wait, getTimesFor } from "./util.js";
 import RenderedObserver from "./rendered-observer.js";
 
 // Start bug detection early if we have a DOM
+let allowDiscrete = "";
 if (globalThis.document) {
-	Bugs.detect();
-}
+	Bugs.detectAll();
 
-const allowDiscrete = globalThis.CSS?.supports?.("transition-behavior", "allow-discrete")
-	? " allow-discrete"
-	: "";
path: src/element-style-observer.js
user: login: LeaVerou
body: Just because it seems it's not relevant if we have no document.
created_at: 2025-05-22T14:52:43Z
author_association: OWNER
diff_hunk: @@ -5,13 +5,14 @@ import { toArray, wait, getTimesFor } from "./util.js";
 import RenderedObserver from "./rendered-observer.js";
 
 // Start bug detection early if we have a DOM
+let allowDiscrete = "";
 if (globalThis.document) {
-	Bugs.detect();
-}
+	Bugs.detectAll();
 
-const allowDiscrete = globalThis.CSS?.supports?.("transition-behavior", "allow-discrete")
-	? " allow-discrete"
-	: "";
path: src/element-style-observer.js
user: login: benface
body: I agree, but it seems we have redundant checks then. If it's inside that `if`, I would remove the `?.`s, but it kinda feels cleaner to keep them and move it outside the `if` (then it can be a `const` instead of a `let`).
created_at: 2025-05-22T15:04:31Z
author_association: COLLABORATOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-21T12:12:01Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *style-observer* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | f47592f7a3e4e517a54fda8bf96b000afbf3d261 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/style-observer/deploys/682dc2d9e3970f00083ff69e |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-117--style-observer.netlify.app](https://deploy-preview-117--style-observer.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTExNy0tc3R5bGUtb2JzZXJ2ZXIubmV0bGlmeS5hcHAifQ.LdClFDO4dlkX8Yu-YH4T5BvIsbw7GehPpyQTb0VJWdw)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [style-observer Preview](https://deploy-preview-117--style-observer.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/style-observer/configuration/notifications#deploy-notifications)._
author_association: NONE
user: login: LeaVerou
created_at: 2025-05-21T13:31:14Z
body: @benface I ended up doing it as a separate PR as I wanted @DmitrySharabin to check for any oversights before I suggest the changes to you. 
author_association: OWNER
user: login: DmitrySharabin
created_at: 2025-05-21T13:50:09Z
body: Updated the bug name to the one we expose (and used before).
author_association: COLLABORATOR
user: login: benface
created_at: 2025-05-21T14:17:46Z
body: Thanks @LeaVerou, the changes look good to me. However, [this](https://github.com/LeaVerou/style-observer/pull/111#discussion_r2096504793) is still an issue that makes the whole thing not viable at the moment. :/ Do you have any idea how we could address it? Would it be acceptable to keep track of which properties were already registered in a map somewhere, instead of checking with `isRegisteredProperty`? And then maybe we can call `gentleRegisterProperty` before the class is instantiated if we have a DOM, like we do for the bugs detection (making the title of the PR completely false haha).
author_association: COLLABORATOR
user: login: LeaVerou
created_at: 2025-05-21T14:20:21Z
body: @benface I haven't noticed this at all, looking now. Does it affect whether this PR is good to be merged in `remove-side-effects`? If not, we can still merge it and take it from there.
Also, is this an issue introduced by the fix for side effects, or does it exist in the deployed version? 
(I may answer my own questions when I look into it, but figured I'd ask in case)

Edit: Okay, I replied there. It looks like this PR is orthogonal to that issue, we could merge it and fix that after.
author_association: OWNER
user: login: benface
created_at: 2025-05-21T14:28:14Z
body: Oh it doesn't affect whether the current PR is good to be merged in the other one, you're right. 👍 I don't know if the way the deployed version calls `gentleRegisterProperty()` on module load causes a lag that could ever be noticeable or problematic. Happy to investigate further if you want.
author_association: COLLABORATOR
user: login: LeaVerou
created_at: 2025-05-21T14:29:46Z
body: @DmitrySharabin could you please check that all renames have gone through and then Squash & Merge? Thx!
Then we can move discussion to #115
author_association: OWNER