diff --git a/packages/devpage-react/src/components/test-buttons/ForesightButtonVisibility.tsx b/packages/devpage-react/src/components/test-buttons/ForesightButtonVisibility.tsx
index 2fe30cf0..352d9dd2 100644
--- a/packages/devpage-react/src/components/test-buttons/ForesightButtonVisibility.tsx
+++ b/packages/devpage-react/src/components/test-buttons/ForesightButtonVisibility.tsx
@@ -24,7 +24,7 @@ const ForesightButtonVisibility = ({ name }: ForesightButtonVisibilityProps) =>
}}
/>
- Toggles via CSS only — MutationObserver should not unregister.
+ Toggles via CSS only - MutationObserver should not unregister.
)
diff --git a/packages/devpage-react/src/components/ui/ButtonStats.tsx b/packages/devpage-react/src/components/ui/ButtonStats.tsx
index 9cca7622..a4c5b194 100644
--- a/packages/devpage-react/src/components/ui/ButtonStats.tsx
+++ b/packages/devpage-react/src/components/ui/ButtonStats.tsx
@@ -20,7 +20,7 @@ const ButtonStats = ({ hitCount, isPredicted, isCallbackRunning, status }: Butto
{row("hits", hitCount)}
{row("predicted", isPredicted ? "yes" : "no")}
{row("cb running", isCallbackRunning ? "yes" : "no")}
- {row("status", status ?? "—")}
+ {row("status", status ?? "-")}
)
}
diff --git a/packages/devpage-react/src/pages/events/index.tsx b/packages/devpage-react/src/pages/events/index.tsx
index 22dfd428..fa3eb9cd 100644
--- a/packages/devpage-react/src/pages/events/index.tsx
+++ b/packages/devpage-react/src/pages/events/index.tsx
@@ -18,7 +18,6 @@ const MAX_LOG_ENTRIES = 200
const ALL_EVENTS: ForesightEvent[] = [
"elementRegistered",
- "elementReactivated",
"elementUnregistered",
"callbackInvoked",
"callbackCompleted",
@@ -39,9 +38,6 @@ const summarizeEvent = (event: ForesightEventMap[ForesightEvent]): string => {
case "elementRegistered":
return `"${formatElementName(event.state)}" registered`
- case "elementReactivated":
- return `"${formatElementName(event.state)}" reactivated`
-
case "elementUnregistered":
return `"${formatElementName(event.state)}" unregistered (${event.unregisterReason})`
@@ -64,9 +60,7 @@ const summarizeEvent = (event: ForesightEventMap[ForesightEvent]): string => {
const EVENT_COLORS: Partial> = {
elementRegistered: "text-green-700",
- elementReactivated: "text-blue-700",
elementUnregistered: "text-red-700",
- elementDataUpdated: "text-gray-600",
callbackInvoked: "text-amber-700",
callbackCompleted: "text-purple-700",
managerSettingsChanged: "text-cyan-700",
@@ -216,15 +210,9 @@ export default function Events() {
useForesightEvent("elementRegistered", e => {
pushEntry(e.type, summarizeEvent(e), e.timestamp)
})
- useForesightEvent("elementReactivated", e => {
- pushEntry(e.type, summarizeEvent(e), e.timestamp)
- })
useForesightEvent("elementUnregistered", e => {
pushEntry(e.type, summarizeEvent(e), e.timestamp)
})
- useForesightEvent("elementDataUpdated", e => {
- pushEntry(e.type, summarizeEvent(e), Date.now())
- })
useForesightEvent("callbackInvoked", e => {
pushEntry(e.type, summarizeEvent(e), e.timestamp)
})
diff --git a/packages/devpage-react/src/pages/home/ForesightImageButton.tsx b/packages/devpage-react/src/pages/home/ForesightImageButton.tsx
index 1eceaff6..a2da1b5e 100644
--- a/packages/devpage-react/src/pages/home/ForesightImageButton.tsx
+++ b/packages/devpage-react/src/pages/home/ForesightImageButton.tsx
@@ -83,7 +83,7 @@ export const ForesightImageButton = ({ image, setSelectedImage }: ForesightImage
-
+
diff --git a/packages/devpage-react/src/pages/mass/index.tsx b/packages/devpage-react/src/pages/mass/index.tsx
index 92f5e9f6..10605d09 100644
--- a/packages/devpage-react/src/pages/mass/index.tsx
+++ b/packages/devpage-react/src/pages/mass/index.tsx
@@ -75,7 +75,7 @@ const Mass = () => {
{isDebugActive && (
- Debug mode is on with {buttonCount.toLocaleString()} elements — the overlay can tank frame
+ Debug mode is on with {buttonCount.toLocaleString()} elements - the overlay can tank frame
rates.
)}
diff --git a/packages/devpage-vue/src/components/ForesightStats.vue b/packages/devpage-vue/src/components/ForesightStats.vue
index 5a3622a3..86d3e438 100644
--- a/packages/devpage-vue/src/components/ForesightStats.vue
+++ b/packages/devpage-vue/src/components/ForesightStats.vue
@@ -23,7 +23,7 @@ defineProps<{
status
- {{ status ?? "—" }}
+ {{ status ?? "-" }}
diff --git a/packages/devpage-vue/src/views/events/index.vue b/packages/devpage-vue/src/views/events/index.vue
index e377f038..b91698ff 100644
--- a/packages/devpage-vue/src/views/events/index.vue
+++ b/packages/devpage-vue/src/views/events/index.vue
@@ -22,7 +22,6 @@ const MAX_LOG_ENTRIES = 200
const ALL_EVENTS: ForesightEvent[] = [
"elementRegistered",
- "elementReactivated",
"elementUnregistered",
"callbackInvoked",
"callbackCompleted",
@@ -32,9 +31,7 @@ const ALL_EVENTS: ForesightEvent[] = [
const EVENT_COLORS: Partial> = {
elementRegistered: "text-green-700",
- elementReactivated: "text-blue-700",
elementUnregistered: "text-red-700",
- elementDataUpdated: "text-gray-600",
callbackInvoked: "text-amber-700",
callbackCompleted: "text-purple-700",
managerSettingsChanged: "text-cyan-700",
@@ -53,8 +50,6 @@ const summarizeEvent = (event: ForesightEventMap[ForesightEvent]): string => {
switch (event.type) {
case "elementRegistered":
return `"${formatElementName(event.state)}" registered`
- case "elementReactivated":
- return `"${formatElementName(event.state)}" reactivated`
case "elementUnregistered":
return `"${formatElementName(event.state)}" unregistered (${event.unregisterReason})`
case "callbackInvoked":
@@ -87,15 +82,9 @@ const pushEntry = (type: ForesightEvent, summary: string, timestamp: number) =>
useForesightEvent("elementRegistered", e => {
pushEntry(e.type, summarizeEvent(e), e.timestamp)
})
-useForesightEvent("elementReactivated", e => {
- pushEntry(e.type, summarizeEvent(e), e.timestamp)
-})
useForesightEvent("elementUnregistered", e => {
pushEntry(e.type, summarizeEvent(e), e.timestamp)
})
-useForesightEvent("elementDataUpdated", e => {
- pushEntry(e.type, summarizeEvent(e), Date.now())
-})
useForesightEvent("callbackInvoked", e => {
pushEntry(e.type, summarizeEvent(e), e.timestamp)
})
diff --git a/packages/devpage-vue/src/views/foresights/index.vue b/packages/devpage-vue/src/views/foresights/index.vue
index 8d404e60..d66437cb 100644
--- a/packages/devpage-vue/src/views/foresights/index.vue
+++ b/packages/devpage-vue/src/views/foresights/index.vue
@@ -19,7 +19,7 @@ import StaticForesightTargets from "./partials/StaticForesightTargets.vue"
Static targets
Bind elements via :ref="slots[i].setRef"
- — no separate ref management needed.
+ - no separate ref management needed.
diff --git a/packages/docs/docs/debugging/devtools.md b/packages/docs/docs/debugging/devtools.md
index d19f1da6..4bd17e58 100644
--- a/packages/docs/docs/debugging/devtools.md
+++ b/packages/docs/docs/debugging/devtools.md
@@ -57,9 +57,7 @@ ForesightDevtools.initialize({
logging: {
logLocation: "controlPanel", // Where to log the Foresight Events
callbackCompleted: true,
- elementReactivated: true,
callbackInvoked: true,
- elementDataUpdated: false,
elementRegistered: false,
elementUnregistered: false,
managerSettingsChanged: true,
diff --git a/packages/docs/docs/debugging/static-properties.md b/packages/docs/docs/debugging/static-properties.md
index 9ed6c829..df545a15 100644
--- a/packages/docs/docs/debugging/static-properties.md
+++ b/packages/docs/docs/debugging/static-properties.md
@@ -135,21 +135,18 @@ The return will look something like this:
"elementUnregistered": []
},
"2": {
- "elementDataUpdated": []
- },
- "3": {
"mouseTrajectoryUpdate": []
},
- "4": {
+ "3": {
"scrollTrajectoryUpdate": []
},
- "5": {
+ "4": {
"managerSettingsChanged": []
},
- "6": {
+ "5": {
"callbackInvoked": []
},
- "7": {
+ "6": {
"callbackCompleted": []
}
},
diff --git a/packages/docs/docs/events.md b/packages/docs/docs/events.md
index 438ddf1b..2cdcf0c0 100644
--- a/packages/docs/docs/events.md
+++ b/packages/docs/docs/events.md
@@ -115,39 +115,6 @@ type ElementRegisteredEvent = {
---
-#### elementOptionsUpdated
-
-Fired when an already-registered element is re-registered with different options (e.g. changed `name`, `callback`, `reactivateAfter`, or `meta`).
-
-```typescript
-type ElementOptionsUpdatedEvent = {
- type: "elementOptionsUpdated"
- timestamp: number
- element: ForesightElement
- state: ForesightElementState
-}
-```
-
-**Related Types:** [`ForesightElementState`](/docs/getting-started/typescript#foresightelementstate)
-
----
-
-#### elementReactivated
-
-Fired when an element is reactivated after its callback was triggered. This happens after `reactivateAfter` ms (default infinity) or with `ForesightManager.instance.reactivate(element)`.
-
-```typescript
-type ElementReactivatedEvent = {
- type: "elementReactivated"
- timestamp: number
- elementData: ForesightElementData
-}
-```
-
-**Related Types:** [`ForesightElementData`](/docs/getting-started/typescript#foresightelementdata)
-
----
-
#### elementUnregistered
Fired when an element is removed from `ForesightManager`'s tracking. This only happens when the element is removed from the `DOM` or via developer actions like `ForesightManager.instance.unregister(element)`
@@ -166,20 +133,6 @@ type ElementUnregisteredEvent = {
---
-#### elementDataUpdated
-
-Fired when tracked element data changes (bounds/visibility only). Does not fire on any updates regarding `callback` data.
-
-```typescript
-type ElementDataUpdatedEvent = {
- type: "elementDataUpdated"
- elementData: ForesightElementData
- updatedProps: UpdatedDataPropertyNames[] // "bounds" | "visibility"
-}
-```
-
-**Related Types:** [`ForesightElementData`](/docs/getting-started/typescript#foresightelementdata)
-
### Prediction Events
Events fired during movement prediction calculations.
diff --git a/packages/docs/docs/getting-started/what-is-foresightjs.md b/packages/docs/docs/getting-started/what-is-foresightjs.md
index 62c15b5a..d1261ac7 100644
--- a/packages/docs/docs/getting-started/what-is-foresightjs.md
+++ b/packages/docs/docs/getting-started/what-is-foresightjs.md
@@ -79,7 +79,7 @@ ForesightJS is designed for developers who want to squeeze every drop of perform
#### Problem 1: On-Hover Prefetching Still Has Latency
-Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires—time that could be used for prefetching.
+Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires-time that could be used for prefetching.
#### Problem 2: Viewport-Based Prefetching is Wasteful
diff --git a/packages/docs/src/components/ForesightOverview/Overview/Keyboard/SmallButton.tsx b/packages/docs/src/components/ForesightOverview/Overview/Keyboard/SmallButton.tsx
index 0db0c9c0..85eba9ae 100644
--- a/packages/docs/src/components/ForesightOverview/Overview/Keyboard/SmallButton.tsx
+++ b/packages/docs/src/components/ForesightOverview/Overview/Keyboard/SmallButton.tsx
@@ -1,4 +1,4 @@
-import { ForesightManager, type ElementReactivatedEvent } from "js.foresight"
+import { ForesightManager } from "js.foresight"
import React, { useEffect, useRef, useState } from "react"
import styles from "./styles.module.css"
const SmallButton = ({ index }: { index: number }) => {
@@ -22,23 +22,9 @@ const SmallButton = ({ index }: { index: number }) => {
return "Element"
}
- const handleElementReactivated = (e: ElementReactivatedEvent) => {
- if (e.element === cardRef.current) {
- setIsLoaded(false)
- setIsLoading(false)
- }
- }
- useEffect(() => {
- ForesightManager.instance.addEventListener("elementReactivated", handleElementReactivated)
-
- return () => {
- ForesightManager.instance.removeEventListener("elementReactivated", handleElementReactivated)
- }
- }, [])
-
useEffect(() => {
if (cardRef.current) {
- const { unregister } = ForesightManager.instance.register({
+ const { unregister, subscribe, getSnapshot } = ForesightManager.instance.register({
element: cardRef.current,
callback: async () => {
if (!stateRef.current.isLoading && !stateRef.current.isLoaded) {
@@ -55,7 +41,22 @@ const SmallButton = ({ index }: { index: number }) => {
meta: { buttonNr: index },
})
- return () => unregister()
+ let wasPredicted = getSnapshot().isPredicted
+ const unsubscribe = subscribe(() => {
+ const snap = getSnapshot()
+ // Detect reactivation: was predicted, now active and not predicted
+ if (wasPredicted && snap.isActive && !snap.isPredicted) {
+ setIsLoaded(false)
+ setIsLoading(false)
+ }
+
+ wasPredicted = snap.isPredicted
+ })
+
+ return () => {
+ unsubscribe()
+ unregister()
+ }
}
}, [cardRef])
diff --git a/packages/docs/versioned_docs/version-3.4/debugging/devtools.md b/packages/docs/versioned_docs/version-3.4/debugging/devtools.md
index d19f1da6..4bd17e58 100644
--- a/packages/docs/versioned_docs/version-3.4/debugging/devtools.md
+++ b/packages/docs/versioned_docs/version-3.4/debugging/devtools.md
@@ -57,9 +57,7 @@ ForesightDevtools.initialize({
logging: {
logLocation: "controlPanel", // Where to log the Foresight Events
callbackCompleted: true,
- elementReactivated: true,
callbackInvoked: true,
- elementDataUpdated: false,
elementRegistered: false,
elementUnregistered: false,
managerSettingsChanged: true,
diff --git a/packages/docs/versioned_docs/version-3.4/debugging/static-properties.md b/packages/docs/versioned_docs/version-3.4/debugging/static-properties.md
index b10caa11..c8ddab12 100644
--- a/packages/docs/versioned_docs/version-3.4/debugging/static-properties.md
+++ b/packages/docs/versioned_docs/version-3.4/debugging/static-properties.md
@@ -128,9 +128,6 @@ The return will look something like this:
"elementUnregistered": []
},
"2": {
- "elementDataUpdated": []
- },
- "3": {
"mouseTrajectoryUpdate": []
},
"4": {
diff --git a/packages/docs/versioned_docs/version-3.4/events.md b/packages/docs/versioned_docs/version-3.4/events.md
index 3704ca37..2cdcf0c0 100644
--- a/packages/docs/versioned_docs/version-3.4/events.md
+++ b/packages/docs/versioned_docs/version-3.4/events.md
@@ -115,22 +115,6 @@ type ElementRegisteredEvent = {
---
-#### elementReactivated
-
-Fired when an element is reactivated after its callback was triggered. This happens after `reactivateAfter` ms (default infinity) or with `ForesightManager.instance.reactivate(element)`.
-
-```typescript
-type ElementReactivatedEvent = {
- type: "elementReactivated"
- timestamp: number
- elementData: ForesightElementData
-}
-```
-
-**Related Types:** [`ForesightElementData`](/docs/getting-started/typescript#foresightelementdata)
-
----
-
#### elementUnregistered
Fired when an element is removed from `ForesightManager`'s tracking. This only happens when the element is removed from the `DOM` or via developer actions like `ForesightManager.instance.unregister(element)`
@@ -149,20 +133,6 @@ type ElementUnregisteredEvent = {
---
-#### elementDataUpdated
-
-Fired when tracked element data changes (bounds/visibility only). Does not fire on any updates regarding `callback` data.
-
-```typescript
-type ElementDataUpdatedEvent = {
- type: "elementDataUpdated"
- elementData: ForesightElementData
- updatedProps: UpdatedDataPropertyNames[] // "bounds" | "visibility"
-}
-```
-
-**Related Types:** [`ForesightElementData`](/docs/getting-started/typescript#foresightelementdata)
-
### Prediction Events
Events fired during movement prediction calculations.
diff --git a/packages/docs/versioned_docs/version-3.4/getting-started/what-is-foresightjs.md b/packages/docs/versioned_docs/version-3.4/getting-started/what-is-foresightjs.md
index 62c15b5a..d1261ac7 100644
--- a/packages/docs/versioned_docs/version-3.4/getting-started/what-is-foresightjs.md
+++ b/packages/docs/versioned_docs/version-3.4/getting-started/what-is-foresightjs.md
@@ -79,7 +79,7 @@ ForesightJS is designed for developers who want to squeeze every drop of perform
#### Problem 1: On-Hover Prefetching Still Has Latency
-Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires—time that could be used for prefetching.
+Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires-time that could be used for prefetching.
#### Problem 2: Viewport-Based Prefetching is Wasteful
diff --git a/packages/foresightjs-react/src/index.ts b/packages/foresightjs-react/src/index.ts
index 674ebcbc..3003d3e9 100644
--- a/packages/foresightjs-react/src/index.ts
+++ b/packages/foresightjs-react/src/index.ts
@@ -8,11 +8,8 @@ export {
type ForesightEvent,
type ForesightEventMap,
type ElementRegisteredEvent,
- type ElementOptionsUpdatedEvent,
type DeviceStrategyChangedEvent,
- type ElementReactivatedEvent,
type ElementUnregisteredEvent,
- type ElementDataUpdatedEvent,
type CallbackInvokedEvent,
type CallbackCompletedEvent,
type MouseTrajectoryUpdateEvent,
diff --git a/packages/foresightjs-vue/src/composables/useForesight.ts b/packages/foresightjs-vue/src/composables/useForesight.ts
index 5f6ed33e..c978a7fb 100644
--- a/packages/foresightjs-vue/src/composables/useForesight.ts
+++ b/packages/foresightjs-vue/src/composables/useForesight.ts
@@ -20,7 +20,7 @@ import type { MaybeElement } from "../types"
import { resolveElement } from "../utils/resolveElement"
export type UseForesightReturn = ToRefs> & {
- /** Template ref function — bind to an element with `:ref="setRef"`. */
+ /** Template ref function - bind to an element with `:ref="setRef"`. */
setRef: (el: MaybeElement) => void
}
@@ -95,7 +95,7 @@ export const useForesight = (
}
}
- // Watch options for changes — patch without re-registering.
+ // Watch options for changes - patch without re-registering.
// Skip when the raw reference hasn't changed (e.g. getter returning same object).
watch(
() => toValue(options),
diff --git a/packages/foresightjs-vue/src/composables/useForesightEvent.test.ts b/packages/foresightjs-vue/src/composables/useForesightEvent.test.ts
index 80675e40..b9f24554 100644
--- a/packages/foresightjs-vue/src/composables/useForesightEvent.test.ts
+++ b/packages/foresightjs-vue/src/composables/useForesightEvent.test.ts
@@ -119,7 +119,7 @@ describe("useForesightEvent", () => {
wrapper.vm.cb = listener2
await nextTick()
- // Should still be only one subscription — the initial one
+ // Should still be only one subscription - the initial one
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
})
diff --git a/packages/foresightjs-vue/src/composables/useForesightEvent.ts b/packages/foresightjs-vue/src/composables/useForesightEvent.ts
index 3e8c62fa..1242aeef 100644
--- a/packages/foresightjs-vue/src/composables/useForesightEvent.ts
+++ b/packages/foresightjs-vue/src/composables/useForesightEvent.ts
@@ -6,7 +6,7 @@ type ListenerArg = MaybeRef<(event: ForesightEventMap[
/**
* Subscribes to a ForesightManager event for the lifetime of the calling scope.
*
- * The listener is always invoked with its latest reference — no stale closures
+ * The listener is always invoked with its latest reference - no stale closures
* when passed as a `ref()`. Changing `eventType` automatically tears down the
* previous subscription and creates a new one; changing only the `listener`
* does not re-subscribe.
diff --git a/packages/foresightjs-vue/src/composables/useForesights.ts b/packages/foresightjs-vue/src/composables/useForesights.ts
index 793e6ecf..70ed94b5 100644
--- a/packages/foresightjs-vue/src/composables/useForesights.ts
+++ b/packages/foresightjs-vue/src/composables/useForesights.ts
@@ -20,7 +20,7 @@ import { resolveElement } from "../utils/resolveElement"
import type { MaybeElement } from "../types"
export type UseForesightSlot = Readonly & {
- /** Template ref function — bind to an element with `:ref="slot.setRef"`. */
+ /** Template ref function - bind to an element with `:ref="slot.setRef"`. */
setRef: (el: MaybeElement) => void
}
@@ -38,7 +38,7 @@ type Slot = {
* The array length determines the number of slots.
*
* Returns a reactive array of `UseForesightSlot` objects. Each slot contains:
- * - `setRef` — a template ref function to bind an element (`:ref="slot.setRef"`)
+ * - `setRef` - a template ref function to bind an element (`:ref="slot.setRef"`)
* - All `ForesightElementState` properties (`isPredicted`, `hitCount`, etc.)
*
* @example
diff --git a/packages/foresightjs-vue/src/index.ts b/packages/foresightjs-vue/src/index.ts
index 8ee46869..2e0a6af0 100644
--- a/packages/foresightjs-vue/src/index.ts
+++ b/packages/foresightjs-vue/src/index.ts
@@ -8,11 +8,8 @@ export {
type ForesightEvent,
type ForesightEventMap,
type ElementRegisteredEvent,
- type ElementOptionsUpdatedEvent,
type DeviceStrategyChangedEvent,
- type ElementReactivatedEvent,
type ElementUnregisteredEvent,
- type ElementDataUpdatedEvent,
type CallbackInvokedEvent,
type CallbackCompletedEvent,
type MouseTrajectoryUpdateEvent,
diff --git a/packages/foresightjs-vue/src/utils/resolveElement.ts b/packages/foresightjs-vue/src/utils/resolveElement.ts
index dd89d8fb..87a06e41 100644
--- a/packages/foresightjs-vue/src/utils/resolveElement.ts
+++ b/packages/foresightjs-vue/src/utils/resolveElement.ts
@@ -16,7 +16,7 @@ export const resolveElement = (target: T): ResolvedEleme
}
const el = (target as ComponentPublicInstance).$el
- // Filter comment nodes — a component with v-if="false" or empty template
+ // Filter comment nodes - a component with v-if="false" or empty template
// leaves a #comment placeholder that has no size or position.
if (el instanceof Node && el.nodeType === Node.COMMENT_NODE) {
return null
diff --git a/packages/js.foresight-devtools/README.md b/packages/js.foresight-devtools/README.md
index f2658268..bba8acc8 100644
--- a/packages/js.foresight-devtools/README.md
+++ b/packages/js.foresight-devtools/README.md
@@ -43,9 +43,7 @@ ForesightDevtools.initialize({
logging: {
logLocation: "controlPanel", // Where to log the Foresight Events
callbackCompleted: true,
- elementReactivated: true,
callbackInvoked: true,
- elementDataUpdated: false,
elementRegistered: false,
elementUnregistered: false,
managerSettingsChanged: true,
diff --git a/packages/js.foresight-devtools/src/helpers/safeSerializeEventData.ts b/packages/js.foresight-devtools/src/helpers/safeSerializeEventData.ts
index 7bbba1ce..46e1a2b0 100644
--- a/packages/js.foresight-devtools/src/helpers/safeSerializeEventData.ts
+++ b/packages/js.foresight-devtools/src/helpers/safeSerializeEventData.ts
@@ -9,7 +9,6 @@ import type {
HitSlop,
ForesightPoint,
ScrollDirection,
- UpdatedDataPropertyNames,
UpdatedManagerSetting,
} from "js.foresight"
@@ -35,15 +34,6 @@ interface ElementRegisteredPayload extends PayloadBase {
meta: Record
}
-interface ElementOptionsUpdatedPayload extends PayloadBase {
- type: "elementOptionsUpdated"
- name: string
- id: string
- state: ForesightElementState
- hitslop: HitSlop
- meta: Record
-}
-
interface ElementUnregisteredEvent extends PayloadBase {
type: "elementUnregistered"
name: string
@@ -53,23 +43,6 @@ interface ElementUnregisteredEvent extends PayloadBase {
wasLastRegisteredElement: boolean
}
-interface ElementReactivatedPayload extends PayloadBase {
- type: "elementReactivated"
- name: string
- id: string
- state: ForesightElementState
- meta: Record
-}
-
-interface ElementDataUpdatedPayload extends PayloadBase {
- type: "elementDataUpdated"
- name: string
- updatedProps: UpdatedDataPropertyNames[]
- state: ForesightElementState
- isIntersecting: boolean
- meta: Record
-}
-
interface CallbackInvokedPayload extends PayloadBase {
type: "callbackInvoked"
name: string
@@ -135,10 +108,7 @@ interface ManagerDataPayload extends PayloadBase {
export type SerializedEventData =
| ElementRegisteredPayload
- | ElementOptionsUpdatedPayload
| ElementUnregisteredEvent
- | ElementReactivatedPayload
- | ElementDataUpdatedPayload
| CallbackInvokedPayload
| CallbackCompletedPayload
| MouseTrajectoryUpdatePayload
@@ -212,32 +182,6 @@ export const safeSerializeEventData = (
? `${event.state.name} - ${getOrdinalSuffix(event.state.registerCount)} time`
: event.state.name,
}
- case "elementOptionsUpdated":
- return {
- type: "elementOptionsUpdated",
- name: event.state.name,
- id: event.element.id || "",
- state: event.state,
- hitslop: event.state.elementBounds.hitSlop,
- localizedTimestamp: new Date(event.timestamp).toLocaleTimeString(),
- meta: event.state.meta,
- logId: logId,
- summary: event.state.name,
- }
- case "elementReactivated":
- return {
- type: "elementReactivated",
- name: event.state.name,
- id: event.element.id || "",
- state: event.state,
- localizedTimestamp: new Date(event.timestamp).toLocaleTimeString(),
- meta: event.state.meta,
- logId: logId,
- summary:
- event.state.registerCount > 1
- ? `${event.state.name} - ${getOrdinalSuffix(event.state.registerCount)} time`
- : event.state.name,
- }
case "elementUnregistered":
return {
type: "elementUnregistered",
@@ -250,18 +194,6 @@ export const safeSerializeEventData = (
logId: logId,
summary: `${event.state.name} - ${event.unregisterReason}`,
}
- case "elementDataUpdated":
- return {
- type: "elementDataUpdated",
- name: event.state.name,
- updatedProps: event.updatedProps || [],
- state: event.state,
- isIntersecting: event.state.isIntersectingWithViewport,
- meta: event.state.meta,
- localizedTimestamp: new Date().toLocaleTimeString(),
- logId: logId,
- summary: `${event.state.name} - ${event.updatedProps.toString()}`,
- }
case "callbackInvoked":
return {
type: "callbackInvoked",
diff --git a/packages/js.foresight-devtools/src/lit-entry/control-panel/element-tab/element-tab.ts b/packages/js.foresight-devtools/src/lit-entry/control-panel/element-tab/element-tab.ts
index 5b2f94e4..2fafa45b 100644
--- a/packages/js.foresight-devtools/src/lit-entry/control-panel/element-tab/element-tab.ts
+++ b/packages/js.foresight-devtools/src/lit-entry/control-panel/element-tab/element-tab.ts
@@ -7,11 +7,8 @@ import type {
CallbackHits,
CallbackHitType,
CallbackInvokedEvent,
- ElementDataUpdatedEvent,
- ElementReactivatedEvent,
ElementRegisteredEvent,
ElementUnregisteredEvent,
- ElementOptionsUpdatedEvent,
} from "js.foresight"
import { ForesightManager, type ForesightElement, type ForesightElementState } from "js.foresight"
@@ -102,6 +99,7 @@ export class ElementTab extends LitElement {
@state() private activeSectionCollapsed = false
@state() private inactiveSectionCollapsed = false
private _abortController: AbortController | null = null
+ private _elementSubscriptions: Map void> = new Map()
private _pendingElementUpdates: Map = new Map()
private _updateDebounceId: ReturnType | null = null
// Cached sorted element lists to avoid repeated filtering in render
@@ -213,61 +211,54 @@ export class ElementTab extends LitElement {
return lines.join("\n")
}
+ private _subscribeToElement(element: ForesightElement): void {
+ if (this._elementSubscriptions.has(element)) {
+ return
+ }
+
+ const unsubscribe = ForesightManager.instance.subscribeToElement(element, () => {
+ const state = ForesightManager.instance.registeredElements.get(element)
+ if (state) {
+ this._pendingElementUpdates.set(element, state)
+ this._scheduleDebouncedUpdate()
+ }
+ })
+
+ if (unsubscribe) {
+ this._elementSubscriptions.set(element, unsubscribe)
+ }
+ }
+
+ private _unsubscribeFromElement(element: ForesightElement): void {
+ this._elementSubscriptions.get(element)?.()
+ this._elementSubscriptions.delete(element)
+ }
+
connectedCallback() {
super.connectedCallback()
this._abortController = new AbortController()
const { signal } = this._abortController
this.updateElementListFromManager()
+ for (const element of this.elementListItems.keys()) {
+ this._subscribeToElement(element)
+ }
+
ForesightManager.instance.addEventListener(
"elementRegistered",
(e: ElementRegisteredEvent) => {
this.elementListItems.set(e.element, e.state)
+ this._subscribeToElement(e.element)
this._elementsCacheDirty = true
this.requestUpdate()
},
{ signal }
)
- ForesightManager.instance.addEventListener(
- "elementOptionsUpdated",
- (e: ElementOptionsUpdatedEvent) => {
- if (!this.applyStateUpdate(e.element, e.state)) {
- return
- }
-
- this.requestUpdate()
- },
- { signal }
- )
-
- ForesightManager.instance.addEventListener(
- "elementDataUpdated",
- (e: ElementDataUpdatedEvent) => {
- if (this.elementListItems.has(e.element)) {
- // Batch updates and debounce to avoid excessive re-renders during scroll
- this._pendingElementUpdates.set(e.element, e.state)
- this._scheduleDebouncedUpdate()
- }
- },
- { signal }
- )
-
- ForesightManager.instance.addEventListener(
- "elementReactivated",
- (e: ElementReactivatedEvent) => {
- if (!this.applyStateUpdate(e.element, e.state)) {
- return
- }
-
- this.requestUpdate()
- },
- { signal }
- )
-
ForesightManager.instance.addEventListener(
"elementUnregistered",
(e: ElementUnregisteredEvent) => {
+ this._unsubscribeFromElement(e.element)
this.elementListItems.delete(e.element)
if (!this.elementListItems.size) {
this.noContentMessage = "No Elements Registered To The Foresight Manager"
@@ -282,9 +273,8 @@ export class ElementTab extends LitElement {
ForesightManager.instance.addEventListener(
"callbackInvoked",
(e: CallbackInvokedEvent) => {
- this.applyStateUpdate(e.element, e.state)
- this._elementsCacheDirty = true
- this.requestUpdate()
+ this._pendingElementUpdates.set(e.element, e.state)
+ this._scheduleDebouncedUpdate()
},
{ signal }
)
@@ -292,30 +282,22 @@ export class ElementTab extends LitElement {
ForesightManager.instance.addEventListener(
"callbackCompleted",
(e: CallbackCompletedEvent) => {
- this.applyStateUpdate(e.element, e.state)
this.handleCallbackCompleted(e.hitType)
- this._elementsCacheDirty = true
- this.requestUpdate()
},
{ signal }
)
}
- private applyStateUpdate(element: ForesightElement, state: ForesightElementState): boolean {
- if (!this.elementListItems.has(element)) {
- return false
- }
-
- this.elementListItems.set(element, state)
- this._elementsCacheDirty = true
-
- return true
- }
-
disconnectedCallback() {
super.disconnectedCallback()
this._abortController?.abort()
this._abortController = null
+
+ for (const unsub of this._elementSubscriptions.values()) {
+ unsub()
+ }
+ this._elementSubscriptions.clear()
+
if (this._updateDebounceId !== null) {
clearTimeout(this._updateDebounceId)
this._updateDebounceId = null
diff --git a/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/log-tab.ts b/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/log-tab.ts
index a7aebf80..22511d26 100644
--- a/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/log-tab.ts
+++ b/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/log-tab.ts
@@ -182,24 +182,12 @@ export class LogTab extends LitElement {
title: "Show element registration events",
icon: FILTER_SVG,
},
- {
- value: "elementOptionsUpdated",
- label: "Element Updated",
- title: "Show element re-registration (options changed) events",
- icon: FILTER_SVG,
- },
{
value: "elementUnregistered",
label: "Element Unregistered",
title: "Show element unregistration events",
icon: FILTER_SVG,
},
- {
- value: "elementReactivated",
- label: "Element Reactivated",
- title: "Show when element gets reactivated after stale time has passed",
- icon: FILTER_SVG,
- },
{
value: "callbackInvoked",
label: "Callback Invoked",
@@ -265,9 +253,7 @@ export class LogTab extends LitElement {
private shouldShowPerformanceWarning(): boolean {
const hasConsoleOutput = this.logLocation === "console" || this.logLocation === "both"
const hasFrequentEvents =
- this.eventsEnabled.mouseTrajectoryUpdate ||
- this.eventsEnabled.scrollTrajectoryUpdate ||
- this.eventsEnabled.elementDataUpdated
+ this.eventsEnabled.mouseTrajectoryUpdate || this.eventsEnabled.scrollTrajectoryUpdate
return hasConsoleOutput && hasFrequentEvents
}
@@ -359,11 +345,8 @@ export class LogTab extends LitElement {
private getEventColor(eventType: ForesightEvent): string {
const colorMap: Record = {
elementRegistered: "#2196f3",
- elementOptionsUpdated: "#2196f3",
- elementReactivated: "#ff9800",
callbackInvoked: "#00bcd4",
callbackCompleted: "#4caf50",
- elementDataUpdated: "#ffc107",
elementUnregistered: "#ff9800",
managerSettingsChanged: "#f44336",
mouseTrajectoryUpdate: "#78909c",
diff --git a/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/single-log.ts b/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/single-log.ts
index 5cf85307..fc01d341 100644
--- a/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/single-log.ts
+++ b/packages/js.foresight-devtools/src/lit-entry/control-panel/log-tab/single-log.ts
@@ -114,11 +114,8 @@ export class SingleLog extends LitElement {
private getLogTypeColor(logType: string): string {
const colorMap: Record = {
elementRegistered: "#2196f3",
- elementOptionsUpdated: "#2196f3",
- elementReactivated: "#ff9800",
callbackInvoked: "#00bcd4",
callbackCompleted: "#4caf50",
- elementDataUpdated: "#ffc107",
elementUnregistered: "#ff9800",
managerSettingsChanged: "#f44336",
mouseTrajectoryUpdate: "#78909c",
@@ -132,10 +129,7 @@ export class SingleLog extends LitElement {
private getEventDisplayName(eventType: string): string {
const eventNames: Record = {
elementRegistered: "Registered",
- elementOptionsUpdated: "Updated",
- elementReactivated: "Reactivated",
elementUnregistered: "Unregistered",
- elementDataUpdated: "Data Updated",
callbackInvoked: "Invoked",
callbackCompleted: "Completed",
mouseTrajectoryUpdate: "Mouse",
diff --git a/packages/js.foresight-devtools/src/lit-entry/debug-overlay/element-overlays.ts b/packages/js.foresight-devtools/src/lit-entry/debug-overlay/element-overlays.ts
index faf43f78..fb662b65 100644
--- a/packages/js.foresight-devtools/src/lit-entry/debug-overlay/element-overlays.ts
+++ b/packages/js.foresight-devtools/src/lit-entry/debug-overlay/element-overlays.ts
@@ -9,11 +9,8 @@ import {
import type {
CallbackCompletedEvent,
CallbackInvokedEvent,
- ElementDataUpdatedEvent,
- ElementReactivatedEvent,
ElementRegisteredEvent,
ElementUnregisteredEvent,
- ElementOptionsUpdatedEvent,
} from "js.foresight"
const STYLE_ID = "foresight-overlay-styles"
@@ -79,6 +76,7 @@ interface CallbackAnimation {
export class ElementOverlays extends LitElement {
@state() private overlayMap: Map = new Map()
@state() private callbackAnimations: Map = new Map()
+ private _elementSubscriptions: Map void> = new Map()
@query("#overlays-container") private containerElement!: HTMLElement
@property({ type: Boolean }) showNameTags = true
@@ -147,56 +145,29 @@ export class ElementOverlays extends LitElement {
ForesightManager.instance.addEventListener(
"elementRegistered",
(e: ElementRegisteredEvent) => {
+ this._subscribeToElement(e.element)
if (e.state.isIntersectingWithViewport) {
this.createOrUpdateElementOverlay(e.element, e.state)
}
},
{ signal }
)
- ForesightManager.instance.addEventListener(
- "elementOptionsUpdated",
- (e: ElementOptionsUpdatedEvent) => {
- if (!e.state.isIntersectingWithViewport || !e.state.isActive) {
- this.removeElementOverlay(e.element)
-
- return
- }
-
- this.createOrUpdateElementOverlay(e.element, e.state)
- },
- { signal }
- )
ForesightManager.instance.addEventListener(
"elementUnregistered",
(e: ElementUnregisteredEvent) => {
+ this._unsubscribeFromElement(e.element)
this.removeElementOverlay(e.element)
},
{ signal }
)
- ForesightManager.instance.addEventListener(
- "elementReactivated",
- (e: ElementReactivatedEvent) => {
- if (e.state.isIntersectingWithViewport) {
- this.createOrUpdateElementOverlay(e.element, e.state)
- }
- },
- { signal }
- )
- ForesightManager.instance.addEventListener(
- "elementDataUpdated",
- (e: ElementDataUpdatedEvent) => {
- if (!e.state.isIntersectingWithViewport) {
- this.removeElementOverlay(e.element)
- return
- }
-
- if (e.state.isActive) {
- this.createOrUpdateElementOverlay(e.element, e.state)
- }
- },
- { signal }
- )
+ // Subscribe to already-registered elements
+ for (const [element, state] of ForesightManager.instance.registeredElements) {
+ this._subscribeToElement(element)
+ if (state.isIntersectingWithViewport && state.isActive) {
+ this.createOrUpdateElementOverlay(element, state)
+ }
+ }
ForesightManager.instance.addEventListener(
"callbackInvoked",
(e: CallbackInvokedEvent) => {
@@ -224,12 +195,12 @@ export class ElementOverlays extends LitElement {
super.attributeChangedCallback(name, oldVal, newVal)
if (name === "hidden") {
if (newVal !== null) {
- // Hidden — remove overlay styles from all elements
+ // Hidden - remove overlay styles from all elements
for (const element of this.overlayMap.keys()) {
this.removeOverlayFromElement(element)
}
} else {
- // Shown — reapply overlay styles to all tracked elements
+ // Shown - reapply overlay styles to all tracked elements
for (const [element] of this.overlayMap) {
const state = ForesightManager.instance.registeredElements.get(element)
if (state) {
@@ -240,6 +211,32 @@ export class ElementOverlays extends LitElement {
}
}
+ private _subscribeToElement(element: ForesightElement): void {
+ if (this._elementSubscriptions.has(element)) {
+ return
+ }
+
+ const unsubscribe = ForesightManager.instance.subscribeToElement(element, () => {
+ const state = ForesightManager.instance.registeredElements.get(element)
+ if (!state || !state.isIntersectingWithViewport || !state.isActive) {
+ this.removeElementOverlay(element)
+
+ return
+ }
+
+ this.createOrUpdateElementOverlay(element, state)
+ })
+
+ if (unsubscribe) {
+ this._elementSubscriptions.set(element, unsubscribe)
+ }
+ }
+
+ private _unsubscribeFromElement(element: ForesightElement): void {
+ this._elementSubscriptions.get(element)?.()
+ this._elementSubscriptions.delete(element)
+ }
+
private applyOverlayToElement(element: ForesightElement, state: ForesightElementState): void {
const { hitSlop } = state.elementBounds
const maxSlop = Math.max(hitSlop.top, hitSlop.right, hitSlop.bottom, hitSlop.left)
@@ -356,6 +353,11 @@ export class ElementOverlays extends LitElement {
disconnectedCallback(): void {
super.disconnectedCallback()
+ for (const unsub of this._elementSubscriptions.values()) {
+ unsub()
+ }
+ this._elementSubscriptions.clear()
+
// Clean up overlay attributes from all tracked elements
for (const element of this.overlayMap.keys()) {
this.removeOverlayFromElement(element)
diff --git a/packages/js.foresight-devtools/src/lit-entry/foresight-devtools.ts b/packages/js.foresight-devtools/src/lit-entry/foresight-devtools.ts
index 6a1ba870..13f1d82d 100644
--- a/packages/js.foresight-devtools/src/lit-entry/foresight-devtools.ts
+++ b/packages/js.foresight-devtools/src/lit-entry/foresight-devtools.ts
@@ -39,11 +39,8 @@ export class ForesightDevtools extends LitElement {
logging: {
logLocation: "controlPanel",
callbackCompleted: true,
- elementReactivated: true,
callbackInvoked: true,
- elementDataUpdated: false,
elementRegistered: false,
- elementOptionsUpdated: false,
elementUnregistered: false,
managerSettingsChanged: true,
mouseTrajectoryUpdate: false,
@@ -172,7 +169,6 @@ export class ForesightDevtools extends LitElement {
this.updateLoggingSetting("callbackCompleted", props.logging.callbackCompleted)
this.updateLoggingSetting("callbackInvoked", props.logging.callbackInvoked)
- this.updateLoggingSetting("elementDataUpdated", props.logging.elementDataUpdated)
this.updateLoggingSetting("elementRegistered", props.logging.elementRegistered)
this.updateLoggingSetting("elementUnregistered", props.logging.elementUnregistered)
this.updateLoggingSetting("managerSettingsChanged", props.logging.managerSettingsChanged)
diff --git a/packages/js.foresight/src/helpers/createInitialState.ts b/packages/js.foresight/src/helpers/createInitialState.ts
index 93438159..7d7664a2 100644
--- a/packages/js.foresight/src/helpers/createInitialState.ts
+++ b/packages/js.foresight/src/helpers/createInitialState.ts
@@ -132,7 +132,7 @@ const EMPTY_DOM_RECT: DOMRectReadOnly = {
*
* Used in two situations:
* 1. The manager refuses to register the element (touch device, limited connection,
- * etc.) — pass `isLimitedConnection` to reflect that.
+ * etc.) - pass `isLimitedConnection` to reflect that.
* 2. Framework wrappers (React, Vue) need an initial snapshot before `register()`
* has run. `register()` requires a real DOM element, which only exists after
* the consumer's first render commits, so the wrapper returns this snapshot
diff --git a/packages/js.foresight/src/index.ts b/packages/js.foresight/src/index.ts
index 2451f380..b1bcf487 100644
--- a/packages/js.foresight/src/index.ts
+++ b/packages/js.foresight/src/index.ts
@@ -15,11 +15,8 @@ export type {
ForesightEvent,
ForesightEventMap,
ElementRegisteredEvent,
- ElementOptionsUpdatedEvent,
DeviceStrategyChangedEvent,
- ElementReactivatedEvent,
ElementUnregisteredEvent,
- ElementDataUpdatedEvent,
CallbackInvokedEvent,
CallbackCompletedEvent,
MouseTrajectoryUpdateEvent,
@@ -34,5 +31,4 @@ export type {
Point as ForesightPoint,
ScrollDirection,
ForesightManagerData,
- UpdatedDataPropertyNames,
} from "./types/types"
diff --git a/packages/js.foresight/src/managers/DesktopHandler.ts b/packages/js.foresight/src/managers/DesktopHandler.ts
index a7dfe5d9..3901ad4f 100644
--- a/packages/js.foresight/src/managers/DesktopHandler.ts
+++ b/packages/js.foresight/src/managers/DesktopHandler.ts
@@ -9,7 +9,6 @@ import type {
ForesightElementInternal,
ForesightElementState,
TrajectoryPositions,
- UpdatedDataPropertyNames,
} from "../types/types"
import { CircularBuffer } from "../helpers/CircularBuffer"
import { DEFAULT_POSITION_HISTORY_SIZE } from "../constants"
@@ -87,7 +86,7 @@ export class DesktopHandler extends ElementObservingModule {
this.checkForMouseHover(entry)
}
- // Must run AFTER handleScrollPrefetch — scroll direction is derived from
+ // Must run AFTER handleScrollPrefetch - scroll direction is derived from
// the difference between the old and new originalRect.
this.handlePositionChangeDataUpdates(entry, positionEntry)
}
@@ -115,13 +114,11 @@ export class DesktopHandler extends ElementObservingModule {
entry: ForesightElementInternal,
positionEntry: PositionObserverEntry
) => {
- const updatedProps: UpdatedDataPropertyNames[] = []
const isNowIntersecting = positionEntry.isIntersecting
const state = entry.state
const patch: Partial = {}
if (state.isIntersectingWithViewport !== isNowIntersecting) {
- updatedProps.push("visibility")
patch.isIntersectingWithViewport = isNowIntersecting
}
@@ -129,7 +126,6 @@ export class DesktopHandler extends ElementObservingModule {
isNowIntersecting &&
!areRectsEqual(positionEntry.boundingClientRect, state.elementBounds.originalRect)
) {
- updatedProps.push("bounds")
patch.elementBounds = {
hitSlop: state.elementBounds.hitSlop,
originalRect: positionEntry.boundingClientRect,
@@ -140,20 +136,11 @@ export class DesktopHandler extends ElementObservingModule {
}
}
- if (updatedProps.length === 0) {
+ if (Object.keys(patch).length === 0) {
return
}
- const next = this.updateElementState(entry, patch)
-
- if (this.hasListeners("elementDataUpdated")) {
- this.emit({
- type: "elementDataUpdated",
- element: entry.element,
- state: next,
- updatedProps,
- })
- }
+ this.updateElementState(entry, patch)
}
protected onDisconnect(): void {
diff --git a/packages/js.foresight/src/managers/ForesightManager.test.ts b/packages/js.foresight/src/managers/ForesightManager.test.ts
index e09e94ff..8d2683bc 100644
--- a/packages/js.foresight/src/managers/ForesightManager.test.ts
+++ b/packages/js.foresight/src/managers/ForesightManager.test.ts
@@ -4,6 +4,7 @@ import type {
ForesightElement,
ForesightElementInternal,
ForesightElementState,
+ ForesightRegisterOptions,
} from "../types/types"
// Mock position-observer before importing ForesightManager
@@ -93,7 +94,9 @@ const runReactivationCycle = async (
expectState(entry.state, { isPredicted: false, isCallbackRunning: false, isActive: true })
}
-const setupBasicTest = (registerOpts: Record = {}) => {
+const setupBasicTest = (
+ registerOpts: Partial> = {}
+) => {
const manager = ForesightManager.initialize()
const element = createMockElement()
const result = manager.register({ element, callback: vi.fn(), ...registerOpts })
@@ -105,13 +108,11 @@ const setupBasicTest = (registerOpts: Record = {}) => {
const setupReactivationTest = (reactivateAfter: number = 5000) => {
const manager = ForesightManager.initialize()
const element = createMockElement()
- const reactivatedListener = vi.fn()
- manager.addEventListener("elementReactivated", reactivatedListener)
manager.register({ element, callback: vi.fn(), reactivateAfter })
const entry = getEntry(manager, element)
- return { manager, element, entry, reactivatedListener }
+ return { manager, element, entry }
}
const setupReactivationAfterFire = async (reactivateAfter: number) => {
@@ -631,7 +632,7 @@ describe("ForesightManager", () => {
describe("Reactivation", () => {
it("should reactivate element after timeout", async () => {
- const { manager, entry, reactivatedListener } = setupReactivationTest(5000)
+ const { manager, entry } = setupReactivationTest(5000)
fire(manager, entry)
@@ -639,23 +640,17 @@ describe("ForesightManager", () => {
await vi.advanceTimersByTimeAsync(100)
expect(entry.state.isActive).toBe(false)
- expect(reactivatedListener).not.toHaveBeenCalled()
// Now advance past reactivateAfter
await vi.advanceTimersByTimeAsync(5000)
expect(entry.state.isActive).toBe(true)
- expect(reactivatedListener).toHaveBeenCalledWith(
- expect.objectContaining({ type: "elementReactivated" })
- )
})
it("should not reactivate if reactivateAfter is Infinity", async () => {
const manager = ForesightManager.initialize()
const element = createMockElement()
- const reactivatedListener = vi.fn()
- manager.addEventListener("elementReactivated", reactivatedListener)
manager.register({ element, callback: vi.fn(), reactivateAfter: Infinity })
const entry = getEntry(manager, element)
@@ -665,7 +660,6 @@ describe("ForesightManager", () => {
await vi.advanceTimersByTimeAsync(100000)
expect(entry.state.isActive).toBe(false)
- expect(reactivatedListener).not.toHaveBeenCalled()
})
it("should support manual reactivation", async () => {
@@ -1237,7 +1231,7 @@ describe("ForesightManager", () => {
fire(manager, entry)
await vi.runAllTimersAsync()
- // Resubscribe — getSnapshot must reflect state that changed while unsubscribed
+ // Resubscribe - getSnapshot must reflect state that changed while unsubscribed
const listener = vi.fn()
result.subscribe(listener)
@@ -1252,7 +1246,7 @@ describe("ForesightManager", () => {
manager.unregister(element)
- // The unregister itself triggers a state update — listener is notified
+ // The unregister itself triggers a state update - listener is notified
const callCount = listener.mock.calls.length
expect(callCount).toBeGreaterThan(0)
@@ -1275,6 +1269,94 @@ describe("ForesightManager", () => {
})
})
+ describe("subscribeToElement", () => {
+ it("should return undefined for an unregistered element", () => {
+ const manager = ForesightManager.initialize()
+ const element = createMockElement()
+
+ const result = manager.subscribeToElement(element, vi.fn())
+ expect(result).toBeUndefined()
+ })
+
+ it("should notify listener when element state changes", () => {
+ const { manager, element, entry } = setupBasicTest()
+ const listener = vi.fn()
+
+ manager.subscribeToElement(element, listener)
+
+ fire(manager, entry)
+ expect(listener).toHaveBeenCalled()
+ })
+
+ it("should stop notifying after unsubscribe", async () => {
+ const { manager, element, entry } = setupBasicTest()
+ const listener = vi.fn()
+
+ const unsubscribe = manager.subscribeToElement(element, listener)!
+ unsubscribe()
+
+ fire(manager, entry)
+ await vi.runAllTimersAsync()
+
+ expect(listener).not.toHaveBeenCalled()
+ })
+
+ it("should allow reading latest state via registeredElements inside listener", () => {
+ const { manager, element, entry } = setupBasicTest()
+ let capturedState: ForesightElementState | undefined
+
+ manager.subscribeToElement(element, () => {
+ capturedState = manager.registeredElements.get(element)
+ })
+
+ fire(manager, entry)
+
+ expect(capturedState).toBeDefined()
+ expect(capturedState!.isPredicted).toBe(true)
+ })
+
+ it("should clean up subscribers when element is unregistered", () => {
+ const { manager, element } = setupBasicTest()
+ const listener = vi.fn()
+
+ manager.subscribeToElement(element, listener)
+
+ manager.unregister(element)
+
+ // Listener is called during unregister (state update to isRegistered: false)
+ const callCount = listener.mock.calls.length
+ expect(callCount).toBeGreaterThan(0)
+
+ // After unregister, subscribing again should return undefined
+ const result = manager.subscribeToElement(element, vi.fn())
+ expect(result).toBeUndefined()
+ })
+
+ it("should support multiple independent subscribers", async () => {
+ const { manager, element, entry } = setupBasicTest({ reactivateAfter: Infinity })
+ const listener1 = vi.fn()
+ const listener2 = vi.fn()
+
+ const unsub1 = manager.subscribeToElement(element, listener1)!
+ manager.subscribeToElement(element, listener2)
+
+ fire(manager, entry)
+ await vi.runAllTimersAsync()
+
+ expect(listener1).toHaveBeenCalled()
+ expect(listener2).toHaveBeenCalled()
+
+ const count1 = listener1.mock.calls.length
+ unsub1()
+
+ // Trigger another state change via manual reactivation
+ manager.reactivate(element)
+
+ expect(listener1).toHaveBeenCalledTimes(count1)
+ expect(listener2.mock.calls.length).toBeGreaterThan(count1)
+ })
+ })
+
describe("updateElementOptions", () => {
it("should update reactivateAfter", () => {
const manager = ForesightManager.initialize()
@@ -1354,44 +1436,40 @@ describe("ForesightManager", () => {
})
it("should reschedule pending reactivation timeout when reactivateAfter changes", async () => {
- const { manager, element, entry, reactivatedListener } =
- await setupReactivationAfterFire(5000)
+ const { manager, element, entry } = await setupReactivationAfterFire(5000)
manager.updateElementOptions(element, { reactivateAfter: 500 })
expect(entry.state.reactivateAfter).toBe(500)
- // Original 5000ms timer should have been cleared
+ // Original 5000ms timer should have been cleared, new 500ms timer fires
await vi.advanceTimersByTimeAsync(500)
- expect(reactivatedListener).toHaveBeenCalledTimes(1)
expect(entry.state.isActive).toBe(true)
})
it("should cancel reactivation when updated with Infinity", async () => {
- const { manager, element, entry, reactivatedListener } =
- await setupReactivationAfterFire(2000)
+ const { manager, element, entry } = await setupReactivationAfterFire(2000)
manager.updateElementOptions(element, { reactivateAfter: Infinity })
await vi.advanceTimersByTimeAsync(10000)
- expect(reactivatedListener).not.toHaveBeenCalled()
expect(entry.state.isActive).toBe(false)
})
it("should not reschedule when there is no pending reactivation timeout", async () => {
- const { manager, element, reactivatedListener } = setupReactivationTest(5000)
+ const { manager, element, entry } = setupReactivationTest(5000)
// Element is still active (no callback fired yet), no pending timeout
+ expect(entry.state.isActive).toBe(true)
manager.updateElementOptions(element, { reactivateAfter: 100 })
- // No spurious reactivation should happen
+ // No spurious reactivation should happen - element should stay active (never deactivated)
await vi.advanceTimersByTimeAsync(200)
- expect(reactivatedListener).not.toHaveBeenCalled()
+ expect(entry.state.isActive).toBe(true)
})
it("should schedule reactivation when updated from Infinity to finite while predicted", async () => {
// Register with Infinity (no reactivation), then fire callback
- const { manager, element, entry, reactivatedListener } =
- await setupReactivationAfterFire(Infinity)
+ const { manager, element, entry } = await setupReactivationAfterFire(Infinity)
// Element is predicted, no timeout exists
expect(entry.state.isPredicted).toBe(true)
@@ -1402,14 +1480,12 @@ describe("ForesightManager", () => {
// Timeout should now be scheduled and fire after 1000ms
await vi.advanceTimersByTimeAsync(1000)
- expect(reactivatedListener).toHaveBeenCalledTimes(1)
expect(entry.state.isActive).toBe(true)
expect(entry.state.isPredicted).toBe(false)
})
it("should not clear pending reactivation timeout when reactivateAfter is unchanged", async () => {
- const { manager, element, entry, reactivatedListener } =
- await setupReactivationAfterFire(2000)
+ const { manager, element, entry } = await setupReactivationAfterFire(2000)
// Element is predicted with a pending reactivation timeout
expect(entry.state.isPredicted).toBe(true)
@@ -1424,7 +1500,6 @@ describe("ForesightManager", () => {
// After the original timeout period, element should reactivate
await vi.advanceTimersByTimeAsync(2000)
- expect(reactivatedListener).toHaveBeenCalledTimes(1)
expect(entry.state.isActive).toBe(true)
expect(entry.state.isPredicted).toBe(false)
})
diff --git a/packages/js.foresight/src/managers/ForesightManager.ts b/packages/js.foresight/src/managers/ForesightManager.ts
index ae2f0e60..f77ddf29 100644
--- a/packages/js.foresight/src/managers/ForesightManager.ts
+++ b/packages/js.foresight/src/managers/ForesightManager.ts
@@ -164,6 +164,30 @@ export class ForesightManager {
return this.eventEmitter.hasListeners(eventType)
}
+ /**
+ * Subscribe to state changes for a specific element.
+ * The listener is called (with no arguments) whenever the element's
+ * immutable state snapshot is replaced. Use {@link registeredElements}
+ * to read the latest state inside the listener.
+ *
+ * @returns An unsubscribe function, or `undefined` if the element is not registered.
+ */
+ public subscribeToElement(
+ element: ForesightElement,
+ listener: () => void
+ ): (() => void) | undefined {
+ const entry = this.elementEntries.get(element)
+ if (!entry) {
+ return undefined
+ }
+
+ entry.subscribers.add(listener)
+
+ return () => {
+ entry.subscribers.delete(listener)
+ }
+ }
+
public get getManagerData(): Readonly {
return {
registeredElements: this.registeredElements,
@@ -314,13 +338,6 @@ export class ForesightManager {
}
}
- this.eventEmitter.emit({
- type: "elementOptionsUpdated",
- timestamp: Date.now(),
- element,
- state: next,
- })
-
return next
}
@@ -340,7 +357,7 @@ export class ForesightManager {
/**
* Replace the immutable state ref for an element and notify subscribers.
- * No-op when every patch value already matches current state — preserves the
+ * No-op when every patch value already matches current state - preserves the
* stable-reference contract relied on by useSyncExternalStore and shallowRef.
*/
private updateElementState(
@@ -448,17 +465,10 @@ export class ForesightManager {
return
}
- const next = this.updateElementState(entry, { isActive: true, isPredicted: false })
+ this.updateElementState(entry, { isActive: true, isPredicted: false })
this.activeElementCount++
this.updateCheckableStatus(entry)
this.currentlyActiveHandler?.observeElement(element)
-
- this.eventEmitter.emit({
- type: "elementReactivated",
- element: element,
- state: next,
- timestamp: Date.now(),
- })
}
private clearReactivateTimeout(entry: ForesightElementInternal): void {
@@ -782,20 +792,13 @@ export class ForesightManager {
return
}
- const next = this.updateElementState(entry, {
+ this.updateElementState(entry, {
elementBounds: {
hitSlop: entry.state.elementBounds.hitSlop,
originalRect: newOriginalRect,
expandedRect,
},
})
-
- this.eventEmitter.emit({
- type: "elementDataUpdated",
- element: entry.element,
- state: next,
- updatedProps: ["bounds" as const],
- })
}
private devLog(message: string): void {
diff --git a/packages/js.foresight/src/types/types.ts b/packages/js.foresight/src/types/types.ts
index 7a20979f..6e113425 100644
--- a/packages/js.foresight/src/types/types.ts
+++ b/packages/js.foresight/src/types/types.ts
@@ -374,10 +374,7 @@ export type ManagerBooleanSettingKeys = {
// This map connects the string name of an event to its data type
export interface ForesightEventMap {
elementRegistered: ElementRegisteredEvent
- elementOptionsUpdated: ElementOptionsUpdatedEvent
- elementReactivated: ElementReactivatedEvent
elementUnregistered: ElementUnregisteredEvent
- elementDataUpdated: ElementDataUpdatedEvent
callbackInvoked: CallbackInvokedEvent
callbackCompleted: CallbackCompletedEvent
mouseTrajectoryUpdate: MouseTrajectoryUpdateEvent
@@ -388,10 +385,7 @@ export interface ForesightEventMap {
export type ForesightEvent =
| "elementRegistered"
- | "elementOptionsUpdated"
- | "elementReactivated"
| "elementUnregistered"
- | "elementDataUpdated"
| "callbackInvoked"
| "callbackCompleted"
| "mouseTrajectoryUpdate"
@@ -411,18 +405,6 @@ export interface ElementRegisteredEvent extends ForesightBaseEvent {
state: ForesightElementState
}
-export interface ElementOptionsUpdatedEvent extends ForesightBaseEvent {
- type: "elementOptionsUpdated"
- element: ForesightElement
- state: ForesightElementState
-}
-
-export interface ElementReactivatedEvent extends ForesightBaseEvent {
- type: "elementReactivated"
- element: ForesightElement
- state: ForesightElementState
-}
-
export interface ElementUnregisteredEvent extends ForesightBaseEvent {
type: "elementUnregistered"
element: ForesightElement
@@ -441,15 +423,6 @@ export interface ElementUnregisteredEvent extends ForesightBaseEvent {
*/
export type ElementUnregisteredReason = "disconnected" | "apiCall" | "devtools" | (string & {})
-export interface ElementDataUpdatedEvent extends Omit {
- type: "elementDataUpdated"
- element: ForesightElement
- state: ForesightElementState
- updatedProps: UpdatedDataPropertyNames[]
-}
-
-export type UpdatedDataPropertyNames = "bounds" | "visibility"
-
export interface CallbackInvokedEvent extends ForesightBaseEvent {
type: "callbackInvoked"
element: ForesightElement