From af1ad6204e0a2fc25ec155b69b32ae35deb21095 Mon Sep 17 00:00:00 2001 From: asakusuma Date: Fri, 18 Jun 2021 00:45:00 -0700 Subject: [PATCH] Use only DOMHighResTimeStamp to calculate duration Previously, we were combining Date.now() and DOMHighResTimeStamp values to calculate duration. These two value types use different clocks, so we should avoid combining the two value types where possible, since using two different clocks leaves us vulnerable to asymetrical issues. The native intersection observer uses DOMHighResTimeStamp, so we should use that type wherever possible in calculations. This change also removes the native-* files, which are superseded by USE_NATIVE_IO and never part of the public API. Thanks to @xg-wang for pointing out some potential issues affecting one clock and not the other, which could in turn cause asymmetrical bugs: https://github.com/w3c/hr-time/issues/115 https://github.com/mdn/content/issues/4713 --- package.json | 2 +- src/interfaces.ts | 61 +++++++- src/intersection-observer.ts | 53 ++++--- src/metal/interfaces.ts | 3 +- src/metal/scheduler.ts | 4 +- src/native-spaniel-observer.ts | 259 --------------------------------- src/native-watcher.ts | 107 -------------- src/spaniel-observer.ts | 57 +++++--- src/watcher.ts | 3 +- 9 files changed, 133 insertions(+), 416 deletions(-) delete mode 100644 src/native-spaniel-observer.ts delete mode 100644 src/native-watcher.ts diff --git a/package.json b/package.json index cd0139c..00a4d80 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "test": "tsc && yarn run build && testem ci && node test/headless/run", "serve": "yarn run build && node test/headless/server/app", - "test:headless": "mocha --require @babel/register test/headless/specs/**/*.js --exit", + "test:headless": "mocha --require @babel/register test/headless/specs/**/*.js test/headless/specs/*.js --exit --timeout 5000", "watch": "broccoli-timepiece exports", "build": "./scripts/build.sh", "stats": "node scripts/size-calc", diff --git a/src/interfaces.ts b/src/interfaces.ts index dffd7a3..b4a97c7 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -28,36 +28,81 @@ export interface SpanielObserverInit { USE_NATIVE_IO?: boolean; } +export interface TimeCompat { + highResTime: number; + unixTime: number; +} + export interface SpanielRecord { target: SpanielTrackedElement; payload: any; thresholdStates: SpanielThresholdState[]; - lastSeenEntry: IntersectionObserverEntry | null; + lastSeenEntry: MaybeInternalIntersectionObserverEntry | null; } export interface SpanielThresholdState { lastSatisfied: Boolean; - lastEntry: IntersectionObserverEntry | null; + lastEntry: MaybeInternalIntersectionObserverEntry | null; threshold: SpanielThreshold; - lastVisible: number; + lastVisible: TimeCompat; visible: boolean; timeoutId?: number; } export interface SpanielIntersectionObserverEntryInit { - time: DOMHighResTimeStamp; - rootBounds: ClientRect; - boundingClientRect: ClientRect; - intersectionRect: ClientRect; + highResTime: DOMHighResTimeStamp; + unixTime: number; + rootBounds: SpanielRect; + boundingClientRect: SpanielRect; + intersectionRect: SpanielRect & ClientRect; target: SpanielTrackedElement; } -export interface SpanielObserverEntry extends IntersectionObserverEntryInit { +export interface SpanielRect extends Partial { + readonly height: number; + readonly width: number; + readonly x: number; + readonly y: number; +} + +export interface SpanielObserverEntry { + isIntersecting: boolean; duration: number; + visibleTime: number; intersectionRatio: number; entering: boolean; label?: string; payload?: any; + unixTime: number; + highResTime: number; + time: number; + target: Element; + boundingClientRect: SpanielRect; + intersectionRect: SpanielRect; + rootBounds: SpanielRect | null; +} + +export interface InternalIntersectionObserverEntry { + time: number; + highResTime: DOMHighResTimeStamp; + target: Element; + boundingClientRect: SpanielRect; + intersectionRect: SpanielRect; + rootBounds: SpanielRect | null; + intersectionRatio: number; + isIntersecting: boolean; +} + +export interface MaybeInternalIntersectionObserverEntry { + time: number; + unixTime?: number; + highResTime?: DOMHighResTimeStamp; + target: Element; + boundingClientRect: SpanielRect; + intersectionRect: SpanielRect & ClientRect; + rootBounds: SpanielRect | null; + intersectionRatio: number; + isIntersecting: boolean; } export interface IntersectionObserverClass { diff --git a/src/intersection-observer.ts b/src/intersection-observer.ts index 5d5f0b6..86a6c95 100644 --- a/src/intersection-observer.ts +++ b/src/intersection-observer.ts @@ -19,15 +19,17 @@ import { DOMRectReadOnly, IntersectionObserverInit, DOMMargin, - SpanielIntersectionObserverEntryInit + SpanielIntersectionObserverEntryInit, + InternalIntersectionObserverEntry, + SpanielRect } from './interfaces'; interface EntryEvent { - entry: IntersectionObserverEntry; + entry: InternalIntersectionObserverEntry; numSatisfiedThresholds: number; } -function marginToRect(margin: DOMMargin): ClientRect { +function marginToRect(margin: DOMMargin): ClientRect & SpanielRect { let { left, right, top, bottom } = margin; return { left, @@ -35,7 +37,9 @@ function marginToRect(margin: DOMMargin): ClientRect { bottom, right, width: right - left, - height: bottom - top + height: bottom - top, + x: left, + y: top }; } @@ -140,13 +144,14 @@ export class SpanielIntersectionObserver implements IntersectionObserver { } } -function addRatio(entryInit: SpanielIntersectionObserverEntryInit): IntersectionObserverEntry { - const { time, rootBounds, boundingClientRect, intersectionRect, target } = entryInit; +function addRatio(entryInit: SpanielIntersectionObserverEntryInit): InternalIntersectionObserverEntry { + const { unixTime, highResTime, rootBounds, boundingClientRect, intersectionRect, target } = entryInit; const boundingArea = boundingClientRect.height * boundingClientRect.width; const intersectionRatio = boundingArea > 0 ? (intersectionRect.width * intersectionRect.height) / boundingArea : 0; return { - time, + time: unixTime, + highResTime, rootBounds, boundingClientRect, intersectionRect, @@ -156,7 +161,7 @@ function addRatio(entryInit: SpanielIntersectionObserverEntryInit): Intersection }; } -function emptyRect(): ClientRect | DOMRect { +function emptyRect(): ClientRect & SpanielRect { return { bottom: 0, height: 0, @@ -174,26 +179,31 @@ export function generateEntry( clientRect: DOMRectReadOnly, el: HTMLElement, rootMargin: DOMMargin -): IntersectionObserverEntry { +): InternalIntersectionObserverEntry { if (el.style.display === 'none') { return { + time: frame.dateNow, + highResTime: frame.highResTime, boundingClientRect: emptyRect(), intersectionRatio: 0, intersectionRect: emptyRect(), isIntersecting: false, rootBounds: emptyRect(), - target: el, - time: frame.timestamp + target: el }; } let { bottom, right } = clientRect; - let rootBounds: ClientRect = { - left: frame.left + rootMargin.left, - top: frame.top + rootMargin.top, + const left = frame.left + rootMargin.left; + const top = frame.top + rootMargin.top; + let rootBounds: SpanielRect & ClientRect = { + left, + top, bottom: rootMargin.bottom, right: rootMargin.right, width: frame.width - (rootMargin.right + rootMargin.left), - height: frame.height - (rootMargin.bottom + rootMargin.top) + height: frame.height - (rootMargin.bottom + rootMargin.top), + y: top, + x: left }; let intersectX = Math.max(rootBounds.left, clientRect.left); @@ -202,9 +212,13 @@ export function generateEntry( let width = Math.min(rootBounds.left + rootBounds.width, clientRect.right) - intersectX; let height = Math.min(rootBounds.top + rootBounds.height, clientRect.bottom) - intersectY; - let intersectionRect: ClientRect = { - left: width >= 0 ? intersectX : 0, - top: intersectY >= 0 ? intersectY : 0, + const interLeft = width >= 0 ? intersectX : 0; + const interTop = intersectY >= 0 ? intersectY : 0; + let intersectionRect: ClientRect & SpanielRect = { + left: interLeft, + top: interTop, + x: interLeft, + y: interTop, width, height, right, @@ -212,7 +226,8 @@ export function generateEntry( }; return addRatio({ - time: frame.timestamp, + unixTime: frame.dateNow, + highResTime: frame.highResTime, rootBounds, target: el, boundingClientRect: marginToRect(clientRect), diff --git a/src/metal/interfaces.ts b/src/metal/interfaces.ts index 0eda640..f8c462a 100644 --- a/src/metal/interfaces.ts +++ b/src/metal/interfaces.ts @@ -51,7 +51,8 @@ export interface ElementSchedulerInterface extends BaseSchedulerInterface { } export interface FrameInterface { - timestamp: number; + dateNow: number; + highResTime: DOMHighResTimeStamp; scrollTop: number; scrollLeft: number; width: number; diff --git a/src/metal/scheduler.ts b/src/metal/scheduler.ts index bac7601..f6ec9f8 100644 --- a/src/metal/scheduler.ts +++ b/src/metal/scheduler.ts @@ -33,7 +33,8 @@ let tokenCounter = 0; export class Frame implements FrameInterface { constructor( - public timestamp: number, + public dateNow: number, + public highResTime: number, public scrollTop: number, public scrollLeft: number, public width: number, @@ -47,6 +48,7 @@ export class Frame implements FrameInterface { const rootMeta = this.revalidateRootMeta(root); return new Frame( Date.now(), + performance.now(), rootMeta.scrollTop, rootMeta.scrollLeft, rootMeta.width, diff --git a/src/native-spaniel-observer.ts b/src/native-spaniel-observer.ts deleted file mode 100644 index 613db8c..0000000 --- a/src/native-spaniel-observer.ts +++ /dev/null @@ -1,259 +0,0 @@ -/* -Copyright 2017 LinkedIn Corp. Licensed under the Apache License, -Version 2.0 (the "License"); you may not use this file except in -compliance with the License. You may obtain a copy of the License -at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -*/ - -import { - DOMMargin, - DOMString, - IntersectionObserverClass, - IntersectionObserverInit, - SpanielObserverEntry, - SpanielObserverInit, - SpanielObserverInterface, - SpanielRecord, - SpanielThreshold, - SpanielThresholdState, - SpanielTrackedElement -} from './interfaces'; -import { generateToken, on, scheduleWork } from './metal/index'; -import w from './metal/window-proxy'; -import { calculateIsIntersecting } from './utils'; - -let emptyRect = { x: 0, y: 0, width: 0, height: 0 }; - -export function DOMMarginToRootMargin(d: DOMMargin): DOMString { - return `${d.top}px ${d.right}px ${d.bottom}px ${d.left}px`; -} - -export class SpanielObserver implements SpanielObserverInterface { - callback: (entries: SpanielObserverEntry[]) => void; - observer: IntersectionObserver; - thresholds: SpanielThreshold[]; - recordStore: { [key: string]: SpanielRecord }; - queuedEntries: SpanielObserverEntry[]; - private paused: boolean; - constructor( - ObserverClass: IntersectionObserverClass, - callback: (entries: SpanielObserverEntry[]) => void, - options?: SpanielObserverInit - ) { - this.paused = false; - this.queuedEntries = []; - this.recordStore = {}; - this.callback = callback; - let { root, rootMargin, threshold } = - options || - ({ - threshold: [] - } as SpanielObserverInit); - rootMargin = rootMargin || '0px'; - let convertedRootMargin: DOMString = - typeof rootMargin !== 'string' ? DOMMarginToRootMargin(rootMargin) : rootMargin; - this.thresholds = threshold.sort((t: SpanielThreshold) => t.ratio); - - let o: IntersectionObserverInit = { - root, - rootMargin: convertedRootMargin, - threshold: this.thresholds.map((t: SpanielThreshold) => t.ratio) - }; - this.observer = new ObserverClass((records: IntersectionObserverEntry[]) => this.internalCallback(records), o); - - if (w.hasDOM) { - on('beforeunload', this.onWindowClosed.bind(this)); - on('hide', this.onTabHidden.bind(this)); - on('show', this.onTabShown.bind(this)); - } - } - private onWindowClosed() { - this.onTabHidden(); - } - private setAllHidden() { - let ids = Object.keys(this.recordStore); - let time = Date.now(); - for (let i = 0; i < ids.length; i++) { - this.handleRecordExiting(this.recordStore[ids[i]], time); - } - this.flushQueuedEntries(); - } - private onTabHidden() { - this.paused = true; - this.setAllHidden(); - } - private onTabShown() { - this.paused = false; - - let ids = Object.keys(this.recordStore); - let time = Date.now(); - for (let i = 0; i < ids.length; i++) { - let entry = this.recordStore[ids[i]].lastSeenEntry; - if (entry) { - let { intersectionRatio, boundingClientRect, rootBounds, intersectionRect, target } = entry; - this.handleObserverEntry({ - intersectionRatio, - boundingClientRect, - time, - rootBounds, - intersectionRect, - target, - isIntersecting: intersectionRatio > 0 - }); - } - } - } - private internalCallback(records: IntersectionObserverEntry[]) { - records.forEach(this.handleObserverEntry.bind(this)); - } - private flushQueuedEntries() { - if (this.queuedEntries.length > 0) { - this.callback(this.queuedEntries); - this.queuedEntries = []; - } - } - private generateSpanielEntry(entry: IntersectionObserverEntry, state: SpanielThresholdState): SpanielObserverEntry { - let { intersectionRatio, time, rootBounds, boundingClientRect, intersectionRect, target, isIntersecting } = entry; - let record = this.recordStore[(target).__spanielId]; - - return { - isIntersecting, - intersectionRatio, - time, - rootBounds, - boundingClientRect, - intersectionRect, - target: target, - duration: 0, - entering: false, - payload: record.payload, - label: state.threshold.label - }; - } - private handleRecordExiting(record: SpanielRecord, time: number = Date.now()) { - record.thresholdStates.forEach((state: SpanielThresholdState) => { - const boundingClientRect = record.lastSeenEntry && record.lastSeenEntry.boundingClientRect; - this.handleThresholdExiting( - { - intersectionRatio: -1, - time, - isIntersecting: false, - payload: record.payload, - label: state.threshold.label, - entering: false, - rootBounds: emptyRect, - boundingClientRect: boundingClientRect || emptyRect, - intersectionRect: emptyRect, - duration: time - state.lastVisible, - target: record.target - }, - state - ); - state.lastSatisfied = false; - state.visible = false; - state.lastEntry = null; - }); - } - private handleThresholdExiting(spanielEntry: SpanielObserverEntry, state: SpanielThresholdState) { - let { time } = spanielEntry; - let hasTimeThreshold = !!state.threshold.time; - if (state.lastSatisfied && (!hasTimeThreshold || (hasTimeThreshold && state.visible))) { - // Make into function - spanielEntry.duration = time - state.lastVisible; - spanielEntry.entering = false; - state.visible = false; - this.queuedEntries.push(spanielEntry); - } - - clearTimeout(state.timeoutId); - } - private handleObserverEntry(entry: IntersectionObserverEntry) { - let { time } = entry; - let target = entry.target; - let record = this.recordStore[target.__spanielId]; - - if (!record) { - return; - } - - record.lastSeenEntry = entry; - - if (!this.paused) { - record.thresholdStates.forEach((state: SpanielThresholdState) => { - // Find the thresholds that were crossed. Since you can have multiple thresholds - // for the same ratio, could be multiple thresholds - let hasTimeThreshold = !!state.threshold.time; - let spanielEntry: SpanielObserverEntry = this.generateSpanielEntry(entry, state); - - const ratioSatisfied = entry.intersectionRatio >= state.threshold.ratio; - const isIntersecting = calculateIsIntersecting(entry); - const isSatisfied = ratioSatisfied && isIntersecting; - - if (isSatisfied != state.lastSatisfied) { - if (isSatisfied) { - spanielEntry.entering = true; - if (hasTimeThreshold) { - state.lastVisible = time; - const timerId: number = Number( - setTimeout(() => { - state.visible = true; - spanielEntry.duration = Date.now() - state.lastVisible; - this.callback([spanielEntry]); - }, state.threshold.time) - ); - state.timeoutId = timerId; - } else { - state.visible = true; - this.queuedEntries.push(spanielEntry); - } - } else { - this.handleThresholdExiting(spanielEntry, state); - } - - state.lastEntry = entry; - state.lastSatisfied = isSatisfied; - } - }); - this.flushQueuedEntries(); - } - } - disconnect() { - this.setAllHidden(); - this.observer.disconnect(); - this.recordStore = {}; - } - unobserve(element: SpanielTrackedElement) { - let record = this.recordStore[element.__spanielId]; - if (record) { - delete this.recordStore[element.__spanielId]; - this.observer.unobserve(element); - scheduleWork(() => { - this.handleRecordExiting(record); - this.flushQueuedEntries(); - }); - } - } - observe(target: Element, payload: any = null) { - let trackedTarget = target as SpanielTrackedElement; - let id = (trackedTarget.__spanielId = trackedTarget.__spanielId || generateToken()); - - this.recordStore[id] = { - target: trackedTarget, - payload, - lastSeenEntry: null, - thresholdStates: this.thresholds.map((threshold: SpanielThreshold) => ({ - lastSatisfied: false, - lastEntry: null, - threshold, - visible: false, - lastVisible: 0 - })) - }; - this.observer.observe(trackedTarget); - return id; - } -} diff --git a/src/native-watcher.ts b/src/native-watcher.ts deleted file mode 100644 index 21ddbfa..0000000 --- a/src/native-watcher.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright 2016 LinkedIn Corp. Licensed under the Apache License, -Version 2.0 (the "License"); you may not use this file except in -compliance with the License. You may obtain a copy of the License -at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -*/ - -import { SpanielObserver } from './native-spaniel-observer'; - -import { - SpanielObserverEntry, - DOMString, - DOMMargin, - SpanielTrackedElement, - IntersectionObserverClass -} from './interfaces'; - -export interface WatcherConfig { - ratio?: number; - time?: number; - rootMargin?: DOMString | DOMMargin; - root?: SpanielTrackedElement; -} - -export type EventName = 'impressed' | 'exposed' | 'visible' | 'impression-complete'; - -export type WatcherCallback = (eventName: EventName, callback: WatcherCallbackOptions) => void; - -export interface Threshold { - label: EventName; - time: number; - ratio: number; -} - -export interface WatcherCallbackOptions { - duration: number; - visibleTime?: number; - boundingClientRect: DOMRectInit; -} - -function onEntry(entries: SpanielObserverEntry[]) { - entries.forEach((entry: SpanielObserverEntry) => { - const { label, duration, boundingClientRect } = entry; - const opts: WatcherCallbackOptions = { - duration, - boundingClientRect - }; - if (entry.entering) { - entry.payload.callback(label, opts); - } else if (entry.label === 'impressed') { - opts.visibleTime = entry.time - entry.duration; - entry.payload.callback('impression-complete', opts); - } - }); -} - -export class Watcher { - observer: SpanielObserver; - constructor(ObserverClass: IntersectionObserverClass, config: WatcherConfig = {}) { - let { time, ratio, rootMargin, root } = config; - - let threshold: Threshold[] = [ - { - label: 'exposed', - time: 0, - ratio: 0 - } - ]; - - if (time) { - threshold.push({ - label: 'impressed', - time, - ratio: ratio || 0 - }); - } - - if (ratio) { - threshold.push({ - label: 'visible', - time: 0, - ratio - }); - } - - this.observer = new SpanielObserver(ObserverClass, onEntry, { - rootMargin, - threshold, - root - }); - } - watch(el: Element, callback: WatcherCallback) { - this.observer.observe(el, { - callback - }); - } - unwatch(el: Element) { - this.observer.unobserve(el as SpanielTrackedElement); - } - disconnect() { - this.observer.disconnect(); - } -} diff --git a/src/spaniel-observer.ts b/src/spaniel-observer.ts index 1c50b49..76f1e80 100644 --- a/src/spaniel-observer.ts +++ b/src/spaniel-observer.ts @@ -13,6 +13,7 @@ import { DOMMargin, DOMString, IntersectionObserverInit, + MaybeInternalIntersectionObserverEntry, SpanielObserverEntry, SpanielObserverInit, SpanielObserverInterface, @@ -98,17 +99,12 @@ export class SpanielObserver implements SpanielObserverInterface { this.paused = true; this.setAllHidden(); } - // Generate a timestamp using the same relative origin time as the backing intersection observer - // native IO timestamps are relative to navigation start, whereas the spaniel polyfill uses unix - // timestamps, relative to 00:00:00 UTC on 1 January 1970 - private generateObserverTimestamp() { - return this.usingNativeIo ? Math.floor(performance.now()) : Date.now(); - } private _onTabShown() { this.paused = false; let ids = Object.keys(this.recordStore); - let time = this.generateObserverTimestamp(); + const highResTime = performance.now(); + const unixTime = Date.now(); for (let i = 0; i < ids.length; i++) { let entry = this.recordStore[ids[i]].lastSeenEntry; if (entry) { @@ -116,7 +112,9 @@ export class SpanielObserver implements SpanielObserverInterface { this.handleObserverEntry({ intersectionRatio, boundingClientRect, - time, + time: unixTime, + highResTime, + unixTime, isIntersecting, rootBounds, intersectionRect, @@ -134,20 +132,31 @@ export class SpanielObserver implements SpanielObserverInterface { this.queuedEntries = []; } } - private generateSpanielEntry(entry: IntersectionObserverEntry, state: SpanielThresholdState): SpanielObserverEntry { + private generateSpanielEntry( + entry: MaybeInternalIntersectionObserverEntry, + state: SpanielThresholdState + ): SpanielObserverEntry { let { intersectionRatio, rootBounds, boundingClientRect, intersectionRect, isIntersecting, time, target } = entry; let record = this.recordStore[(target).__spanielId]; - const timeOrigin = w.performance.timeOrigin || w.performance.timing.navigationStart; - const unixTime = this.usingNativeIo ? Math.floor(timeOrigin + time) : time; + const unixTime = this.usingNativeIo + ? Math.floor(w.performance.timeOrigin || w.performance.timing.navigationStart + time) + : time; + const highResTime = this.usingNativeIo ? time : entry.highResTime; + if (!highResTime) { + throw new Error('Missing intersection entry timestamp'); + } return { intersectionRatio, isIntersecting, + unixTime, time: unixTime, + highResTime, rootBounds, boundingClientRect, intersectionRect, target: target, duration: 0, + visibleTime: isIntersecting ? time : -1, entering: false, payload: record.payload, label: state.threshold.label @@ -155,20 +164,24 @@ export class SpanielObserver implements SpanielObserverInterface { } private handleRecordExiting(record: SpanielRecord) { const time = Date.now(); + const perfTime = performance.now(); record.thresholdStates.forEach((state: SpanielThresholdState) => { const boundingClientRect = record.lastSeenEntry && record.lastSeenEntry.boundingClientRect; this.handleThresholdExiting( { intersectionRatio: -1, isIntersecting: false, + unixTime: time, time, + highResTime: perfTime, payload: record.payload, label: state.threshold.label, entering: false, rootBounds: emptyRect, boundingClientRect: boundingClientRect || emptyRect, intersectionRect: emptyRect, - duration: time - state.lastVisible, + visibleTime: state.lastVisible.unixTime, + duration: perfTime - state.lastVisible.highResTime, target: record.target }, state @@ -179,11 +192,12 @@ export class SpanielObserver implements SpanielObserverInterface { }); } private handleThresholdExiting(spanielEntry: SpanielObserverEntry, state: SpanielThresholdState) { - let { time } = spanielEntry; + let { highResTime } = spanielEntry; let hasTimeThreshold = !!state.threshold.time; if (state.lastSatisfied && (!hasTimeThreshold || (hasTimeThreshold && state.visible))) { // Make into function - spanielEntry.duration = time - state.lastVisible; + spanielEntry.duration = highResTime - state.lastVisible.highResTime; + spanielEntry.visibleTime = state.lastVisible.unixTime; spanielEntry.entering = false; state.visible = false; this.queuedEntries.push(spanielEntry); @@ -191,7 +205,7 @@ export class SpanielObserver implements SpanielObserverInterface { clearTimeout(state.timeoutId); } - private handleObserverEntry(entry: IntersectionObserverEntry) { + private handleObserverEntry(entry: MaybeInternalIntersectionObserverEntry) { let target = entry.target; let record = this.recordStore[target.__spanielId]; @@ -218,11 +232,15 @@ export class SpanielObserver implements SpanielObserverInterface { if (isSatisfied) { spanielEntry.entering = true; if (hasTimeThreshold) { - state.lastVisible = spanielEntry.time; + state.lastVisible = { + highResTime: spanielEntry.highResTime, + unixTime: spanielEntry.unixTime + }; const timerId: number = Number( setTimeout(() => { state.visible = true; - spanielEntry.duration = Date.now() - state.lastVisible; + spanielEntry.duration = performance.now() - state.lastVisible.highResTime; + spanielEntry.visibleTime = state.lastVisible.unixTime; this.callback([spanielEntry]); }, state.threshold.time) ); @@ -285,7 +303,10 @@ export class SpanielObserver implements SpanielObserverInterface { lastEntry: null, threshold, visible: false, - lastVisible: 0 + lastVisible: { + unixTime: 0, + highResTime: -1 + } })) }; this.observer.observe(trackedTarget); diff --git a/src/watcher.ts b/src/watcher.ts index 03af208..53d6418 100644 --- a/src/watcher.ts +++ b/src/watcher.ts @@ -45,13 +45,12 @@ function onEntry(entries: SpanielObserverEntry[]) { const opts: WatcherCallbackOptions = { duration, boundingClientRect, - visibleTime: entry.time, + visibleTime: entry.visibleTime, intersectionRect }; if (entry.entering) { entry.payload.callback(label, opts); } else if (entry.label === 'impressed') { - opts.visibleTime = entry.time - entry.duration; entry.payload.callback('impression-complete', opts); } });