diff --git a/modules/ad_spiroBidAdapter.ts b/modules/ad_spiroBidAdapter.ts index 1604e0d1ece..0bd4258327f 100644 --- a/modules/ad_spiroBidAdapter.ts +++ b/modules/ad_spiroBidAdapter.ts @@ -8,6 +8,7 @@ import { import { ajax } from '../src/ajax.js'; import { AUDIO, BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { isPlainObject, logWarn } from '../src/utils.js'; +import type { ORTBResponse } from '../src/prebid.public.js'; /** * Adspiro bid parameters. @@ -106,7 +107,7 @@ export const spec: BidderSpec = { if (!isPlainObject(serverResponse?.body)) { return []; } - const { bids } = converter.fromORTB({ request: request.data, response: serverResponse.body }) as ExtendedResponse; + const { bids } = converter.fromORTB({ request: request.data, response: serverResponse.body as ORTBResponse }) as ExtendedResponse; return bids; }, diff --git a/modules/asterioBidAdapter.ts b/modules/asterioBidAdapter.ts index 09290422025..941b4848234 100644 --- a/modules/asterioBidAdapter.ts +++ b/modules/asterioBidAdapter.ts @@ -43,6 +43,9 @@ type AsterioServerBid = { mediaType?: string; format?: string; adomain?: string[]; + vastXml?: string; + native?: AsterioNativeBid; + meta?: Record; }; type AsterioNativeImage = { diff --git a/modules/multibid/index.ts b/modules/multibid/index.ts index a7b103baa53..daa19ba0bfc 100644 --- a/modules/multibid/index.ts +++ b/modules/multibid/index.ts @@ -3,6 +3,7 @@ * @module modules/multibid */ +import type { Bid } from '../../src/bidfactory.ts'; import { config } from '../../src/config.js'; import { getHook, setupBeforeHookFnOnce } from '../../src/hook.js'; import { deepAccess, deepSetValue, getUniqueIdentifierStr, groupBy, logWarn } from '../../src/utils.js'; @@ -210,7 +211,7 @@ export function aliasedBiddersFirst(a, b) { * @param {Number} adUnitBidLimit bidder targeting limit, default set to 0 * @param {Boolean} hasModified default set to false, this hook modifies targeting and sets to true */ -export function targetBidPoolHook(fn, bidsReceived, winReducer, adUnitBidLimit = 0, hasModified = false, winSorter = highestDesirabilityFirst) { +export function targetBidPoolHook(fn, bidsReceived: Bid[], winReducer, adUnitBidLimit = 0, hasModified = false, winSorter = highestDesirabilityFirst) { if (!config.getConfig('multibid')) resetMultiConfig(); if (hasMultibid) { const modifiedBids = []; diff --git a/src/adRendering.ts b/src/adRendering.ts index ea310758959..5d1857d6210 100644 --- a/src/adRendering.ts +++ b/src/adRendering.ts @@ -214,14 +214,14 @@ export const getRenderingData = hook('sync', function (bidResponse: Bid, options }; }); -function prepareBidForRendering(bidResponse: Bid, options?: RenderOptions): Bid { +function prepareBidForRendering(bidResponse: T, options?: RenderOptions): T { const { ad, adUrl, cpm, originalCpm, safeRenderer } = bidResponse; const repl = { AUCTION_PRICE: originalCpm || cpm, CLICKTHROUGH: options?.clickUrl || '' }; - const result = { + const result: T = { ...bidResponse, ad: replaceMacros(ad, repl), adUrl: replaceMacros(adUrl, repl) diff --git a/src/adapterManager.ts b/src/adapterManager.ts index c3699037980..48c824051b8 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -80,7 +80,7 @@ export const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { CLIENT: 'client', SERVER: 'server' -}; +} as const; export const dep = { isAllowed: isActivityAllowed, @@ -440,7 +440,7 @@ export function getS2SBidderSet(s2sConfigs) { * @returns {Object} return.client - Array of bidder codes that should be routed to client adapters. * @returns {Object} return.server - Array of bidder codes that should be routed to server adapters. */ -export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}) { +export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}): { [PARTITIONS.CLIENT]: string[], [PARTITIONS.SERVER]: string[] } { const serverBidders = getS2SBidders(s2sConfigs); return getBidderCodes(adUnits).reduce((memo, bidder) => { const partition = serverBidders.has(bidder) ? PARTITIONS.SERVER : PARTITIONS.CLIENT; diff --git a/src/types/local/safeframe.d.ts b/src/types/local/safeframe.d.ts new file mode 100644 index 00000000000..8c44be83f04 --- /dev/null +++ b/src/types/local/safeframe.d.ts @@ -0,0 +1,37 @@ +import type { SafeFrameGeometry } from '../../utils.ts'; + +declare global { + interface Window { + $sf?: SafeFrameAPI; + } + + interface SafeFrameAPI { + ext: SafeFrameExternalAPI; + } + + interface SafeFrameExternalAPI { + register( + width: number, + height: number, + notify?: (event: string, data?: unknown) => void + ): void; + + expand(dimensions?: { + t?: number; + b?: number; + l?: number; + r?: number; + push?: boolean; + }): void; + + collapse(): void; + + geom(): SafeFrameGeometry; + + inViewPercentage(): number; + + status(): string; + + supports(feature: string): boolean; + } +} diff --git a/src/types/local/scheduler.d.ts b/src/types/local/scheduler.d.ts new file mode 100644 index 00000000000..f75505a464b --- /dev/null +++ b/src/types/local/scheduler.d.ts @@ -0,0 +1,21 @@ +export {}; +declare global { + type TaskPriority = "user-blocking" | "user-visible" | "background"; + + interface SchedulerPostTaskOptions { + delay?: number; + priority?: TaskPriority; + signal?: AbortSignal; + } + + interface Scheduler { + postTask(callback: (...params: P) => T, options?: SchedulerPostTaskOptions, ...arguments: P): Promise; + } + + interface Window { + /** + * https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1249 + */ + scheduler?: Scheduler; + } +} diff --git a/src/utils.js b/src/utils.ts similarity index 75% rename from src/utils.js rename to src/utils.ts index 29e4a07682c..639ad45204e 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,12 +1,12 @@ import { config } from './config.js'; import { PbPromise } from './utils/promise.js'; -import deepAccess from 'dlv/index.js'; +import dlv from 'dlv/index.js'; import { isArray, isFn, isPlainObject, isStr } from './utils/objects.js'; import * as logging from './utils/logging.js'; import * as debug from './utils/debug.js'; -export { deepAccess }; +export const deepAccess: (obj: any, path: string | ReadonlyArray, def?: any) => any = dlv; export { dset as deepSetValue } from 'dset'; export * from './utils/objects.js'; export { getWinDimensions, resetWinDimensions, getScreenOrientation } from './utils/winDimensions.js'; @@ -44,21 +44,21 @@ const prebidInternal = {}; /** * Returns object that is used as internal prebid namespace */ -export function getPrebidInternal() { +export function getPrebidInternal(): Record { return prebidInternal; } /* utility method to get incremental integer starting from 1 */ -var getIncrementalInteger = (function () { +var getIncrementalInteger = (function (): () => number { var count = 0; - return function () { + return function (): number { count++; return count; }; })(); // generate a random string (to be used as a dynamic JSONP callback) -export function getUniqueIdentifierStr() { +export function getUniqueIdentifierStr(): string { return getIncrementalInteger() + Math.random().toString(16).substring(2); } @@ -68,17 +68,17 @@ export function getUniqueIdentifierStr() { * and y is replaced with a random hexadecimal digit from 8 to b. * https://gist.github.com/jed/982883 via node-uuid */ -export function generateUUID(placeholder) { +export function generateUUID(placeholder?: number | string): string { return placeholder - ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16) - : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); + ? (Number(placeholder) ^ _getRandomData() >> Number(placeholder) / 4).toString(16) + : ('' + [1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); } /** * Returns random data using the Crypto API if available and Math.random if not * Method is from https://gist.github.com/jed/982883 like generateUUID, direct link https://gist.github.com/jed/982883#gistcomment-45104 */ -function _getRandomData() { +function _getRandomData(): number { if (window && window.crypto && window.crypto.getRandomValues) { return window.crypto.getRandomValues(new Uint8Array(1))[0] % 16; } else { @@ -86,14 +86,14 @@ function _getRandomData() { } } -export function getBidIdParameter(key, paramsObj) { +export function getBidIdParameter(key: string, paramsObj: Record): string { return paramsObj?.[key] || ''; } // parse a query string object passed in bid params // bid params should be an object such as {key: "value", key1 : "value1"} // aliases to formatQS -export function parseQueryStringParameters(queryObj) { +export function parseQueryStringParameters(queryObj: Record): string { let result = ''; for (var k in queryObj) { if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; } @@ -103,7 +103,7 @@ export function parseQueryStringParameters(queryObj) { } // transform an AdServer targeting bids into a query string to send to the adserver -export function transformAdServerTargetingObj(targeting) { +export function transformAdServerTargetingObj(targeting: Record): string { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { return Object.keys(targeting) @@ -116,7 +116,7 @@ export function transformAdServerTargetingObj(targeting) { /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]' */ -export function sizesToSizeTuples(sizes) { +export function sizesToSizeTuples(sizes: string | number[] | number[][] | unknown): [number, number][] { if (typeof sizes === 'string') { // multiple sizes will be comma-separated return sizes @@ -138,56 +138,56 @@ export function sizesToSizeTuples(sizes) { * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ -export function parseSizesInput(sizeObj) { +export function parseSizesInput(sizeObj: string | number[] | number[][]): string[] { return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString); } -export function sizeTupleToSizeString(size) { +export function sizeTupleToSizeString(size: [number, number]): string { return size[0] + 'x' + size[1]; } // Parse a GPT style single size array, (i.e [300, 250]) // into an AppNexus style string, (i.e. 300x250) -export function parseGPTSingleSizeArray(singleSize) { +export function parseGPTSingleSizeArray(singleSize: number[]): string | undefined { if (isValidGPTSingleSize(singleSize)) { return sizeTupleToSizeString(singleSize); } } -export function sizeTupleToRtbSize(size) { +export function sizeTupleToRtbSize(size: [number, number]): { w: number; h: number } { return { w: size[0], h: size[1] }; } // Parse a GPT style single size array, (i.e [300, 250]) // into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250}) -export function parseGPTSingleSizeArrayToRtbSize(singleSize) { +export function parseGPTSingleSizeArrayToRtbSize(singleSize: number[]): { w: number; h: number } | undefined { if (isValidGPTSingleSize(singleSize)) { return sizeTupleToRtbSize(singleSize); } } -function isValidGPTSingleSize(singleSize) { +function isValidGPTSingleSize(singleSize: unknown): singleSize is [number, number] { // if we aren't exactly 2 items in this array, it is invalid return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1])); } -export function getWindowTop() { +export function getWindowTop(): Window { return window.top; } -export function getWindowSelf() { +export function getWindowSelf(): Window { return window.self; } -export function getWindowLocation() { +export function getWindowLocation(): Location { return window.location; } -export function getDocument() { +export function getDocument(): Document { return document; } -export function canAccessWindowTop() { +export function canAccessWindowTop(): boolean { try { if (internal.getWindowTop().location.href) { return true; @@ -202,7 +202,7 @@ export function canAccessWindowTop() { * @param {Window} [win] * @returns {Window} */ -export function getFallbackWindow(win) { +export function getFallbackWindow(win?: Window): Window { if (win) { return win; } @@ -220,7 +220,7 @@ export const createIframe = (() => { frameBorder: '0', allowtransparency: 'true' }; - return (doc, attrs, style = {}) => { + return (doc: Document, attrs: Record, style: Record = {}) => { const f = doc.createElement('iframe'); Object.assign(f, Object.assign({}, DEFAULTS, attrs)); Object.assign(f.style, style); @@ -228,7 +228,7 @@ export const createIframe = (() => { }; })(); -export function createInvisibleIframe(doc = document, attrs = {}, style = {}) { +export function createInvisibleIframe(doc: Document = document, attrs = {}, style = {}): HTMLIFrameElement { return createIframe(doc, { id: getUniqueIdentifierStr(), width: 0, @@ -248,7 +248,7 @@ export function createInvisibleIframe(doc = document, attrs = {}, style = {}) { * Check if a given parameter name exists in query string * and if it does return the value */ -export function getParameterByName(name) { +export function getParameterByName(name: string): string { return parseQS(getWindowLocation().search)[name] || ''; } @@ -258,12 +258,12 @@ export function getParameterByName(name) { * @param {*} object object to test * @return {Boolean} if object is empty */ -export function isEmpty(object) { +export function isEmpty(object: unknown): boolean { if (!object) return true; if (isArray(object) || isStr(object)) { - return !(object.length > 0); + return !((object as { length: number }).length > 0); } - return Object.keys(object).length <= 0; + return Object.keys(object as Record).length <= 0; } /** @@ -271,7 +271,7 @@ export function isEmpty(object) { * @param str string to test * @returns {boolean} if string is empty */ -export function isEmptyStr(str) { +export function isEmptyStr(str: unknown): boolean { return isStr(str) && (!str || str.length === 0); } @@ -282,13 +282,13 @@ export function isEmptyStr(str) { * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object. * @returns {void} */ -export function _each(object, fn) { +export function _each(object: T[] | Record, fn: (value: T, key: any) => void): void { if (isFn(object?.forEach)) return object.forEach(fn, this); Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } -export function contains(a, obj) { - return isFn(a?.includes) && a.includes(obj); +export function contains(a: unknown, obj: unknown): boolean { + return isFn((a as { includes?: unknown })?.includes) && (a as { includes: (o: unknown) => boolean }).includes(obj); } /** @@ -298,7 +298,7 @@ export function contains(a, obj) { * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object. * @return {Array} */ -export function _map(object, callback) { +export function _map(object: T[] | Record, callback: (value: T, key: any, obj?: T[] | Record) => R): R[] { if (isFn(object?.map)) return object.map(callback); return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)); } @@ -311,7 +311,7 @@ export function _map(object, callback) { * @param {Boolean} [asLastChildChild] * @return {HTML Element} */ -export function insertElement(elm, doc, target, asLastChildChild) { +export function insertElement(elm: Node, doc?: Document, target?: string, asLastChildChild?: boolean): Node | undefined { doc = doc || document; let parentEl; if (target) { @@ -337,9 +337,9 @@ export function insertElement(elm, doc, target, asLastChildChild) { * @param {Number} [timeout] * @returns {Promise} */ -export function waitForElementToLoad(element, timeout) { - let timer = null; - return new PbPromise((resolve) => { +export function waitForElementToLoad(element: HTMLElement, timeout?: number): Promise { + let timer: number | null = null; + return new PbPromise((resolve) => { const onLoad = function() { element.removeEventListener('load', onLoad); element.removeEventListener('error', onLoad); @@ -364,7 +364,7 @@ export function waitForElementToLoad(element, timeout) { * `'omit'` for a cookieless request — the image fallback (which cannot omit cookies) is then skipped. */ -export function politeTriggerPixel(url, credentials = 'include') { +export function politeTriggerPixel(url: string, credentials: RequestCredentials = 'include'): void { const triggerSync = () => { if (window.fetch && window.Request) { try { @@ -384,7 +384,7 @@ export function politeTriggerPixel(url, credentials = 'include') { runBackgroundTask(triggerSync); } -export function triggerPixel(url, done, timeout) { +export function triggerPixel(url: string, done?: () => void, timeout?: number): void { const img = new Image(); if (done && internal.isFn(done)) { waitForElementToLoad(img, timeout).then(done); @@ -396,7 +396,7 @@ export function triggerPixel(url, done, timeout) { * Run a task at low priority when supported by the browser, or immediately as fallback. * @param {function} task */ -export function runBackgroundTask(task) { +export function runBackgroundTask(task: () => void): void { const scheduler = window.scheduler; if (scheduler?.postTask) { scheduler.postTask(task, { priority: 'background' }).catch(() => task()); @@ -414,7 +414,7 @@ export function runBackgroundTask(task) { * (though could be for other purposes) * @param {string} htmlCode snippet of HTML code used for tracking purposes */ -export function insertHtmlIntoIframe(htmlCode) { +export function insertHtmlIntoIframe(htmlCode: string): void { if (!htmlCode) { return; } @@ -434,7 +434,7 @@ export function insertHtmlIntoIframe(htmlCode) { * @param encode * @return {string} HTML snippet that contains the img src = set to `url` */ -export function createTrackPixelHtml(url, encode = encodeURI) { +export function createTrackPixelHtml(url: string, encode: (url: string) => string = encodeURI): string { if (!url) { return ''; } @@ -470,7 +470,7 @@ const MACRO = /\$\{([^}#$&+,/:;=?@]+)\}/g; * @param url * @return {string} */ -export function encodeMacroURI(url) { +export function encodeMacroURI(url): string { let encoded = ''; let from = 0; for (const match of url.matchAll(MACRO)) { @@ -481,15 +481,15 @@ export function encodeMacroURI(url) { return encoded + encodeURI(url.slice(from)); } -export function uniques(value, index, arry) { +export function uniques(value: T, index: number, arry: T[]) { return arry.indexOf(value) === index; } -export function flatten(a, b) { +export function flatten(a: T[], b: T | T[]) { return a.concat(b); } -export function getBidRequest(id, bidderRequests) { +export function getBidRequest(id: string, bidderRequests: Array<{ bids: any[] }>): Record | undefined { if (!id) { return; } @@ -497,29 +497,25 @@ export function getBidRequest(id, bidderRequests) { .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id)); } -export function getValue(obj, key) { +export function getValue(obj: Record, key: string): unknown { return obj[key]; } -export function getBidderCodes(adUnits) { +export function getBidderCodes(adUnits: Array<{ bids?: Array<{ bidder?: string }> }>): string[] { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques); } -export function isGptPubadsDefined() { - if (window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)) { - return true; - } +export function isGptPubadsDefined(): boolean { + return !!(window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)); } -export function isApnGetTagDefined() { - if (window.apntag && isFn(window.apntag.getTag)) { - return true; - } +export function isApnGetTagDefined(): boolean { + return !!(window.apntag && isFn(window.apntag.getTag)); } -export const sortByHighestCpm = (a, b) => { +export const sortByHighestCpm = (a: { cpm: number }, b: { cpm: number }): number => { return b.cpm - a.cpm; }; @@ -529,7 +525,7 @@ export const sortByHighestCpm = (a, b) => { * https://bost.ocks.org/mike/shuffle/ * istanbul ignore next */ -export function shuffle(array) { +export function shuffle(array: T[]): T[] { let counter = array.length; // while there are elements in the array @@ -549,7 +545,7 @@ export function shuffle(array) { return array; } -export function inIframe() { +export function inIframe(): boolean { try { return internal.getWindowSelf() !== internal.getWindowTop(); } catch (e) { @@ -560,7 +556,7 @@ export function inIframe() { /** * https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf */ -export function isSafeFrameWindow() { +export function isSafeFrameWindow(): boolean { if (!inIframe()) { return false; } @@ -569,50 +565,60 @@ export function isSafeFrameWindow() { return !!(ws.$sf && ws.$sf.ext); } +/** + * Geometry of a SafeFrame container, as returned by `$sf.ext.geom()`. + */ +export interface SafeFrameGeometry { + self?: { x: number; y: number; w: number; h: number }; + exp?: { t: number; b: number; l: number; r: number }; + win?: { w: number; h: number }; + view?: { x: number; y: number; w: number; h: number }; +} + /** * Returns the result of calling the function $sf.ext.geom() if it exists * @see https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf — 5.4 Function $sf.ext.geom * @returns {Object | undefined} geometric information about the container */ -export function getSafeframeGeometry() { +export function getSafeframeGeometry(): SafeFrameGeometry | undefined { try { const ws = getWindowSelf(); - return (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : undefined; + return isFn(ws.$sf.ext.geom) ? ws.$sf.ext.geom() : undefined; } catch (e) { logError('Error getting SafeFrame geometry', e); return undefined; } } -export function isSafariBrowser() { +export function isSafariBrowser(): boolean { return /^((?!chrome|chromium|android|crios|fxios).)*safari/i.test(navigator.userAgent); } -export function isFirefoxBrowser() { +export function isFirefoxBrowser(): boolean { return /firefox|fxios/i.test(navigator.userAgent); } -export function isChromeIOSBrowser() { +export function isChromeIOSBrowser(): boolean { return /crios|crmo/i.test(navigator.userAgent); } -export function replaceMacros(str, subs) { +export function replaceMacros(str: string | undefined, subs: Record): string | undefined { if (!str) return; return Object.entries(subs).reduce((str, [key, val]) => { return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || ''); }, str); } -export function replaceAuctionPrice(str, cpm) { +export function replaceAuctionPrice(str: string | undefined, cpm: number | string): string | undefined { return replaceMacros(str, { AUCTION_PRICE: cpm }); } -export function replaceClickThrough(str, clicktag) { +export function replaceClickThrough(str: string | undefined, clicktag: string): string | undefined { if (!str || !clicktag || typeof clicktag !== 'string') return; return str.replace(/\${CLICKTHROUGH}/g, clicktag); } -export function timestamp() { +export function timestamp(): number { return new Date().getTime(); } @@ -620,7 +626,7 @@ export function timestamp() { * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now * @returns {number} */ -export function getPerformanceNow() { +export function getPerformanceNow(): number { return (window.performance && window.performance.now && window.performance.now()) || 0; } @@ -632,7 +638,7 @@ export function getPerformanceNow() { * @param {Window} w The window object used to perform the api call. default to window.self * @returns {number} */ -export function getDomLoadingDuration(w) { +export function getDomLoadingDuration(w?: Window): number { let domLoadingDuration = -1; w = w || getWindowSelf(); @@ -655,14 +661,14 @@ export function getDomLoadingDuration(w) { * When the deviceAccess flag config option is false, no cookies should be read or set * @returns {boolean} */ -export function hasDeviceAccess() { +export function hasDeviceAccess(): boolean { return config.getConfig('deviceAccess') !== false; } /** * @returns {(boolean|undefined)} */ -export function checkCookieSupport() { +export function checkCookieSupport(): boolean | undefined { // eslint-disable-next-line no-restricted-properties if (window.navigator.cookieEnabled || !!document.cookie.length) { return true; @@ -681,15 +687,15 @@ export function checkCookieSupport() { * @param {number} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ -export function delayExecution(func, numRequiredCalls) { +export function delayExecution(func: (...args: A) => void, numRequiredCalls: number): (...args: A) => void { if (numRequiredCalls < 1) { throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`); } let numCalls = 0; - return function () { + return function (this: unknown, ...args: A): void { numCalls++; if (numCalls === numRequiredCalls) { - func.apply(this, arguments); + func.apply(this, args); } }; } @@ -700,9 +706,10 @@ export function delayExecution(func, numRequiredCalls) { * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} */ -export function groupBy(xs, key) { - return xs.reduce(function(rv, x) { - (rv[x[key]] = rv[x[key]] || []).push(x); +export function groupBy(xs: T[], key: string): Record { + return xs.reduce(function(rv: Record, x) { + const k = String((x as Record)[key]); + (rv[k] = rv[k] || []).push(x); return rv; }, {}); } @@ -712,7 +719,7 @@ export function groupBy(xs, key) { * @param mediaTypes mediaTypes parameter to validate * @return If object is valid */ -export function isValidMediaTypes(mediaTypes) { +export function isValidMediaTypes(mediaTypes: Record): boolean { const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video', 'audio']; const SUPPORTED_STREAM_TYPES = ['instream', 'outstream']; @@ -736,7 +743,7 @@ export function isValidMediaTypes(mediaTypes) { * @param {string} bidder code * @return {Array} user configured param for the given bidder adunit configuration */ -export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { +export function getUserConfiguredParams(adUnits: Array<{ code: string; bids: Array<{ bidder: string; params?: Record }> }>, adUnitCode: string, bidder: string): Record[] { return adUnits .filter(adUnit => adUnit.code === adUnitCode) .flatMap((adUnit) => adUnit.bids) @@ -744,26 +751,28 @@ export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { .map((bidderData) => bidderData.params || {}); } -export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; +export const compareCodeAndSlot = (slot: googletag.Slot, adUnitCode: string) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; /** * Returns filter function to match adUnitCode in slot * @param slot GoogleTag slot * @return filter function */ -export function isAdUnitCodeMatchingSlot(slot) { +export function isAdUnitCodeMatchingSlot(slot: googletag.Slot): (adUnitCode: string) => boolean { const customGptSlotMatching = config.getConfig('customGptSlotMatching'); const match = isFn(customGptSlotMatching) && customGptSlotMatching(slot); - return isFn(match) ? match : (adUnitCode) => compareCodeAndSlot(slot, adUnitCode); + return isFn(match) ? match : (adUnitCode: string) => compareCodeAndSlot(slot, adUnitCode); } /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped + * @param {string} adUnit.code the ad unit code + * @param {Object} [adUnit.mediaTypes] the ad unit media types * @param {string} bidder bidder code that is not compatible with the adUnit * @return {string} warning message to display when condition is met */ -export function unsupportedBidderMessage(adUnit, bidder) { +export function unsupportedBidderMessage(adUnit: { code: string; mediaTypes?: Record }, bidder: string): string { const mediaType = Object.keys(adUnit.mediaTypes || { 'banner': 'banner' }).join(', '); return ` @@ -777,7 +786,7 @@ export function unsupportedBidderMessage(adUnit, bidder) { * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ -export function cleanObj(obj) { +export function cleanObj(obj: Record): Record { return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined')); } @@ -786,7 +795,7 @@ export function cleanObj(obj) { * @param obj the original object * @param properties An array of desired properties */ -export function pick(obj, properties) { +export function pick(obj: Record, properties: ReadonlyArray any)>): Record { if (typeof obj !== 'object') { return {}; } @@ -805,7 +814,7 @@ export function pick(obj, properties) { let value = obj[prop]; if (typeof properties[i + 1] === 'function') { - value = properties[i + 1](value, newObj); + value = (properties[i + 1] as (...args: any[]) => any)(value, newObj); } if (typeof value !== 'undefined') { newObj[newProp] = value; @@ -815,7 +824,7 @@ export function pick(obj, properties) { }, {}); } -export function parseQS(query) { +export function parseQS(query: string): Record { return !query ? {} : query .replace(/^\?/, '') .split('&') @@ -832,7 +841,7 @@ export function parseQS(query) { }, {}); } -export function formatQS(query) { +export function formatQS(query: Record): string { return Object .keys(query) .map(k => Array.isArray(query[k]) @@ -841,7 +850,16 @@ export function formatQS(query) { .join('&'); } -export function parseUrl(url, options) { +export function parseUrl(url: string, options?: Record): { + href: string; + protocol: string; + hostname: string; + port: number; + pathname: string; + search: any; + hash: string; + host: string; +} { const parsed = document.createElement('a'); if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { parsed.href = url; @@ -856,18 +874,18 @@ export function parseUrl(url, options) { hostname: parsed.hostname, port: +parsed.port, pathname: parsed.pathname.replace(/^(?!\/)/, '/'), - search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''), + search: (qsAsString ? parsed.search : internal.parseQS(parsed.search || '')) as any, hash: (parsed.hash || '').replace(/^#/, ''), host: parsed.host || window.location.host }; } -export function buildUrl(obj) { +export function buildUrl(obj: { protocol?: string; host?: string; hostname?: string; port?: number; pathname?: string; search?: string | Record; hash?: string }): string { return (obj.protocol || 'http') + '://' + (obj.host || obj.hostname + (obj.port ? `:${obj.port}` : '')) + (obj.pathname || '') + - (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') + + (obj.search ? `?${internal.formatQS(obj.search as Record)}` : '') + (obj.hash ? `#${obj.hash}` : ''); } @@ -879,7 +897,7 @@ export function buildUrl(obj) { * @param {boolean} [options.checkTypes=false] - If set, two objects with identical properties but different constructors will *not* be considered equivalent. * @returns {boolean} - Returns `true` if the objects are equivalent, `false` otherwise. */ -export function deepEqual(obj1, obj2, { checkTypes = false } = {}) { +export function deepEqual(obj1: any, obj2: any, { checkTypes = false } = {}): boolean { // Quick reference check if (obj1 === obj2) return true; @@ -930,7 +948,7 @@ export function deepEqual(obj1, obj2, { checkTypes = false } = {}) { return true; } -export function mergeDeep(target, ...sources) { +export function mergeDeep(target: Record, ...sources: any[]): any { for (let i = 0; i < sources.length; i++) { const source = sources[i]; if (!isPlainObject(source)) { @@ -941,7 +959,7 @@ export function mergeDeep(target, ...sources) { return target; } -function mergeDeepHelper(target, source) { +function mergeDeepHelper(target: any, source: any): void { // quick check if (!isPlainObject(target) || !isPlainObject(source)) { return; @@ -965,9 +983,10 @@ function mergeDeepHelper(target, source) { target[key] = [...val]; } else { // deduplicate + const targetArr = target[key] as any[]; val.forEach(obj => { - if (!target[key].some(item => deepEqual(item, obj))) { - target[key].push(obj); + if (!targetArr.some(item => deepEqual(item, obj))) { + targetArr.push(obj); } }); } @@ -985,10 +1004,8 @@ function mergeDeepHelper(target, source) { * @param seed (optional) * @returns {string} */ -export function cyrb53Hash(str, seed = 0) { - // IE doesn't support imul - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill - const imul = function(opA, opB) { +export function cyrb53Hash(str: string, seed: number = 0): string { + const imul = function(opA: number, opB: number) { if (isFn(Math.imul)) { return Math.imul(opA, opB); } else { @@ -1008,7 +1025,7 @@ export function cyrb53Hash(str, seed = 0) { let h1 = 0xdeadbeef ^ seed; let h2 = 0x41c6ce57 ^ seed; - for (let i = 0, ch; i < str.length; i++) { + for (let i = 0, ch: number; i < str.length; i++) { ch = str.charCodeAt(i); h1 = imul(h1 ^ ch, 2654435761); h2 = imul(h2 ^ ch, 1597334677); @@ -1023,13 +1040,13 @@ export function cyrb53Hash(str, seed = 0) { * @param data * @returns {any} */ -export function safeJSONParse(data) { +export function safeJSONParse(data: string): T | undefined { try { return JSON.parse(data); } catch (e) {} } -export function safeJSONEncode(data) { +export function safeJSONEncode(data: unknown): string { try { return JSON.stringify(data); } catch (e) { @@ -1045,18 +1062,18 @@ export function safeJSONEncode(data) { * By default, the first argument is used as key. * @return {*} */ -export function memoize(fn, key = function (arg) { return arg; }) { +export function memoize any>(fn: T, key: (...args: any[]) => any = (arg) => arg): ((...args: Parameters) => ReturnType) & { clear: () => void; invalidate: (...args: any[]) => void } { const cache = new Map(); - const memoized = function () { - const cacheKey = key.apply(this, arguments); + const memoized = function (this: unknown, ...args: Parameters): ReturnType { + const cacheKey = key.apply(this, args); if (!cache.has(cacheKey)) { - cache.set(cacheKey, fn.apply(this, arguments)); + cache.set(cacheKey, fn.apply(this, args)); } return cache.get(cacheKey); - }; + } as ((...args: Parameters) => ReturnType) & { clear: () => void; invalidate: (...args: any[]) => void }; memoized.clear = cache.clear.bind(cache); - memoized.invalidate = function () { - cache.delete(key.apply(this, arguments)); + memoized.invalidate = function (this: unknown, ...args: any[]) { + cache.delete(key.apply(this, args)); }; return memoized; } @@ -1067,7 +1084,7 @@ export function memoize(fn, key = function (arg) { return arg; }) { * @param {string} timeUnit defaults to days (or 'd'), use 'm' for minutes. Any parameter that isn't 'd' or 'm' will return Date.now(). * @returns {number} */ -export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') { +export function getUnixTimestampFromNow(timeValue: number = 0, timeUnit: 'm' | 'd' = 'd'): number { const acceptableUnits = ['m', 'd']; if (acceptableUnits.indexOf(timeUnit) < 0) { return Date.now(); @@ -1084,7 +1101,7 @@ export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') { * @param {Object} obj the object * @returns {Array} */ -export function convertObjectToArray(obj) { +export function convertObjectToArray(obj: Record): Array> { return Object.keys(obj).map(key => { return { [key]: obj[key] }; }); @@ -1095,7 +1112,7 @@ export function convertObjectToArray(obj) { * @param {HTMLScriptElement} script * @param {object} attributes */ -export function setScriptAttributes(script, attributes) { +export function setScriptAttributes(script: HTMLScriptElement, attributes: Record): void { Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v)); } @@ -1109,7 +1126,7 @@ export function setScriptAttributes(script, attributes) { * if the element is not found, return the index of the first element that's greater; * if no greater element exists, return `arr.length`) */ -export function binarySearch(arr, el, key = (el) => el) { +export function binarySearch(arr: T[], el: T, key: (el: T) => unknown = (el) => el): number { let left = 0; let right = arr.length && arr.length - 1; const target = key(el); @@ -1135,7 +1152,7 @@ export function binarySearch(arr, el, key = (el) => el) { * @param {Set} checkedObjects - A set of properties that have already been checked. * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise. */ -export function hasNonSerializableProperty(obj, checkedObjects = new Set()) { +export function hasNonSerializableProperty(obj: Record, checkedObjects: Set = new Set()): boolean { for (const key in obj) { const value = obj[key]; const type = typeof value; @@ -1173,8 +1190,8 @@ export function hasNonSerializableProperty(obj, checkedObjects = new Set()) { * @param {String} key - Key of nested property. * @returns {any|undefined} - Value of nested property. */ -export function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { +export function setOnAny(collection: unknown[], key: string): unknown { + for (let i = 0, result: unknown; i < collection.length; i++) { result = deepAccess(collection[i], key); if (result) { return result; @@ -1183,7 +1200,7 @@ export function setOnAny(collection, key) { return undefined; } -export function extractDomainFromHost(pageHost) { +export function extractDomainFromHost(pageHost: string): string | null { let domain = null; try { const domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); @@ -1201,11 +1218,11 @@ export function extractDomainFromHost(pageHost) { return domain; } -export function triggerNurlWithCpm(bid, cpm) { +export function triggerNurlWithCpm(bid: { nurl?: string }, cpm: number | string): void { if (isStr(bid.nurl) && bid.nurl !== '') { bid.nurl = bid.nurl.replace( /\${AUCTION_PRICE}/, - cpm + String(cpm) ); politeTriggerPixel(bid.nurl); } diff --git a/src/utils/cachedApiWrapper.js b/src/utils/cachedApiWrapper.js deleted file mode 100644 index a36026ed98e..00000000000 --- a/src/utils/cachedApiWrapper.js +++ /dev/null @@ -1,26 +0,0 @@ -export function CachedApiWrapper(getTarget, props) { - const wrapper = {}; - let data = {}; - const children = []; - Object.entries(props).forEach(([key, value]) => { - if (value != null && typeof value === 'object') { - const child = new CachedApiWrapper(() => getTarget()?.[key], value); - wrapper[key] = child.obj; - children.push(child.reset); - } else if (value === true) { - Object.defineProperty(wrapper, key, { - get() { - if (!data.hasOwnProperty(key)) { - data[key] = getTarget()?.[key]; - } - return data[key]; - } - }); - } - }); - this.obj = wrapper; - this.reset = function () { - children.forEach(reset => reset()); - data = {}; - }; -} diff --git a/src/utils/cachedApiWrapper.ts b/src/utils/cachedApiWrapper.ts new file mode 100644 index 00000000000..e5e329f3275 --- /dev/null +++ b/src/utils/cachedApiWrapper.ts @@ -0,0 +1,48 @@ +/** + * Selects the properties of `Target` to cache: `true` for a value, a nested object for a child object. + */ +export type CachedApiProps = { + [K in keyof Target]?: boolean | (NonNullable extends object ? CachedApiProps> : never); +}; + +/** + * A value that has a subset of the properties of `Target`, recursively. + */ +type Subset = { [K in keyof Target]?: Subset> }; + +/** + * Wraps the properties of the object returned by `getTarget` flagged in `props` with getters that + * read through to the target once, and serve the cached value until `reset()` is called. + */ +export class CachedApiWrapper & Record, never>> { + readonly obj: T; + readonly reset: () => void; + + constructor(getTarget: () => Target, props: CachedApiProps) { + const read = (key: string): unknown => (getTarget() as Record | undefined)?.[key]; + const wrapper: Record = {}; + let data: Record = {}; + const children: Array<() => void> = []; + Object.entries(props).forEach(([key, value]) => { + if (value != null && typeof value === 'object') { + const child = new CachedApiWrapper>(() => read(key), value as CachedApiProps); + wrapper[key] = child.obj; + children.push(child.reset); + } else if (value === true) { + Object.defineProperty(wrapper, key, { + get() { + if (!data.hasOwnProperty(key)) { + data[key] = read(key); + } + return data[key]; + } + }); + } + }); + this.obj = wrapper as T; + this.reset = () => { + children.forEach(reset => reset()); + data = {}; + }; + } +} diff --git a/src/utils/cpm.js b/src/utils/cpm.ts similarity index 80% rename from src/utils/cpm.js rename to src/utils/cpm.ts index a52cdc45a97..08c4661ff58 100644 --- a/src/utils/cpm.js +++ b/src/utils/cpm.ts @@ -7,14 +7,14 @@ import { logError } from '../utils.js'; * precedence as publisher settings: `getOwn(bidderCode)` wins, otherwise adapter vs bidder depending on * `adjustAlternateBids` on the adapter scope. */ -export function getBidAdjustmentFn(bid, bs, key, bidRequest) { +export function getBidAdjustmentFn(bid: any, bs: any, key: string, bidRequest?: any): any { const adapterCode = bid?.adapterCode; const bidderCode = bid?.bidderCode || bidRequest?.bidder; const adjustAlternateBids = bs.get(bid?.adapterCode, 'adjustAlternateBids'); return bs.getOwn(bidderCode, key) || bs.get(adjustAlternateBids ? adapterCode : bidderCode, key); } -export function adjustCpm(cpm, bidResponse, bidRequest, { index = auctionManager.index, bs = bidderSettings } = {}) { +export function adjustCpm(cpm: number, bidResponse: any, bidRequest?: any, { index = auctionManager.index, bs = bidderSettings }: { index?: any; bs?: any } = {}): number { bidRequest = bidRequest || index.getBidRequest(bidResponse); const bidCpmAdjustment = getBidAdjustmentFn(bidResponse, bs, 'bidCpmAdjustment', bidRequest); if (bidCpmAdjustment && typeof bidCpmAdjustment === 'function') { diff --git a/src/utils/focusTimeout.js b/src/utils/focusTimeout.ts similarity index 80% rename from src/utils/focusTimeout.js rename to src/utils/focusTimeout.ts index b448f3d6201..7223e1bff16 100644 --- a/src/utils/focusTimeout.js +++ b/src/utils/focusTimeout.ts @@ -1,6 +1,6 @@ -let outOfFocusStart = null; // enforce null otherwise it could be undefined and the callback wouldn't execute +let outOfFocusStart: number | null = null; // enforce null otherwise it could be undefined and the callback wouldn't execute let timeOutOfFocus = 0; -let suspendedTimeouts = []; +let suspendedTimeouts: Array<{ callback: () => void; startTime: number; setTimerId: (id: ReturnType) => void }> = []; function trackTimeOutOfFocus() { if (document.hidden) { @@ -15,7 +15,7 @@ function trackTimeOutOfFocus() { document.addEventListener('visibilitychange', trackTimeOutOfFocus); -export function reset() { +export function reset(): void { outOfFocusStart = null; timeOutOfFocus = 0; suspendedTimeouts = []; @@ -30,7 +30,7 @@ export function reset() { * @param {number} [milliseconds] - Minimum duration (in milliseconds) that the callback will be executed after * @returns {function(): number} - Getter function for current timer id */ -export function setFocusTimeout(callback, milliseconds) { +export function setFocusTimeout(callback: () => void, milliseconds?: number): () => ReturnType { const startTime = timeOutOfFocus; let timerId = setTimeout(() => { if (timeOutOfFocus === startTime && outOfFocusStart == null) { diff --git a/src/utils/gdpr.js b/src/utils/gdpr.ts similarity index 67% rename from src/utils/gdpr.js rename to src/utils/gdpr.ts index 6151861a67b..3cce0663ed8 100644 --- a/src/utils/gdpr.js +++ b/src/utils/gdpr.ts @@ -4,7 +4,7 @@ * @param gdprConsent GDPR consent data * @returns {boolean} true if the gdprConsent is null-y; or GDPR does not apply; or if purpose 1 consent was given. */ -export function hasPurpose1Consent(gdprConsent) { +export function hasPurpose1Consent(gdprConsent?: { gdprApplies?: boolean; vendorData?: { purpose?: { consents?: Record } } } | null): boolean { if (gdprConsent?.gdprApplies) { return gdprConsent?.vendorData?.purpose?.consents?.[1] === true; } diff --git a/src/utils/ipUtils.js b/src/utils/ipUtils.ts similarity index 83% rename from src/utils/ipUtils.js rename to src/utils/ipUtils.ts index cbb0c38c298..1f5c6a7c600 100644 --- a/src/utils/ipUtils.js +++ b/src/utils/ipUtils.ts @@ -1,4 +1,4 @@ -export function scrubIPv4(ip) { +export function scrubIPv4(ip?: string | null): string | null { if (!ip) { return null; } @@ -11,7 +11,7 @@ export function scrubIPv4(ip) { return null; } - const mask = []; + const mask: number[] = []; for (let i = 0; i < 4; i++) { const n = Math.max(0, Math.min(8, ones - (i * 8))); mask.push((0xff << (8 - n)) & 0xff); @@ -22,7 +22,7 @@ export function scrubIPv4(ip) { return maskedIP.join('.'); } -export function scrubIPv6(ip) { +export function scrubIPv6(ip?: string | null): string | null { if (!ip) { return null; } @@ -40,7 +40,7 @@ export function scrubIPv6(ip) { return null; } - const mask = []; + const mask: number[] = []; for (let i = 0; i < 8; i++) { const n = Math.max(0, Math.min(16, ones - (i * 16))); mask.push((0xffff << (16 - n)) & 0xffff); diff --git a/src/utils/objects.ts b/src/utils/objects.ts index abb12f6cbd1..e2d58b22179 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -25,35 +25,35 @@ const toString = Object.prototype.toString; * Return if the object is of the * given type. */ -export function isFn(object): object is AnyFunction { - return typeof object === 'function'; +export function isFn(value: unknown): value is AnyFunction { + return typeof value === 'function'; } -export function isStr(object): object is string { - return typeof object === 'string'; +export function isStr(value: unknown): value is string { + return typeof value === 'string'; } -export const isArray: (object) => object is any[] = Array.isArray.bind(Array); +export const isArray: (value: unknown) => value is any[] = Array.isArray.bind(Array); -export function isNumber(object): object is number { - return typeof object === 'number'; +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; } -export function isPlainObject(object): object is Record { - return toString.call(object) === '[object Object]'; +export function isPlainObject(value: unknown): value is Record { + return toString.call(value) === '[object Object]'; } -export function isBoolean(object): object is boolean { - return typeof object === 'boolean'; +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; } /** * Checks input is integer or not * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger */ -export const isInteger: (value) => value is number = Number.isInteger.bind(Number); +export const isInteger: (value: unknown) => value is number = Number.isInteger.bind(Number); -export function isArrayOfNums(val, size?: L): val is Repeat { +export function isArrayOfNums(val: unknown, size?: L): val is Repeat { // Array.from because every() skips holes in sparse arrays return (isArray(val)) && ((size) ? val.length === size : true) && (Array.from(val).every(v => isInteger(v))); } @@ -62,7 +62,7 @@ export function isArrayOfNums(val, size?: L): val is Repeat 0)) { // Array.from because every() skips holes in sparse arrays if (Array.from(sizes).every(sz => isArrayOfNums(sz, 2))) { diff --git a/src/utils/reducers.js b/src/utils/reducers.ts similarity index 65% rename from src/utils/reducers.js rename to src/utils/reducers.ts index 97102dadf57..8ae395e57fa 100644 --- a/src/utils/reducers.js +++ b/src/utils/reducers.ts @@ -1,19 +1,22 @@ +import type { Bid } from '../bidfactory.ts'; import { DEFAULT_PRIORITY, PRIORITY_INCREMENT } from './desirability.js'; -export function simpleCompare(a, b) { +export type Compare = (a: T, b: T) => number; + +export function simpleCompare(a: T, b: T): number { if (a === b) return 0; return a < b ? -1 : 1; } -export function keyCompare(key = (item) => item) { +export function keyCompare(key: (item: T) => unknown = (item) => item): Compare { return (a, b) => simpleCompare(key(a), key(b)); } -export function reverseCompare(compare = simpleCompare) { +export function reverseCompare(compare: Compare = simpleCompare): Compare { return (a, b) => -compare(a, b) || 0; } -export function tiebreakCompare(...compares) { +export function tiebreakCompare(...compares: Array>): Compare { return function (a, b) { for (const cmp of compares) { const val = cmp(a, b); @@ -23,28 +26,28 @@ export function tiebreakCompare(...compares) { }; } -export function minimum(compare = simpleCompare) { +export function minimum(compare: Compare = simpleCompare): (min: T, item: T) => T { return (min, item) => compare(item, min) < 0 ? item : min; } -export function maximum(compare = simpleCompare) { +export function maximum(compare: Compare = simpleCompare): (min: T, item: T) => T { return minimum(reverseCompare(compare)); } -const cpmCompare = keyCompare((bid) => bid.cpm); -const timestampCompare = keyCompare((bid) => bid.responseTimestamp); +const cpmCompare = keyCompare((bid) => bid.cpm); +const timestampCompare = keyCompare((bid) => bid.responseTimestamp); // This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond -export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))); +export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))); -export function priorityCompare(left, right) { +export function priorityCompare(left: any, right: any): number { const a = left.priority ?? DEFAULT_PRIORITY; const b = right.priority ?? DEFAULT_PRIORITY; if (a === b) return 0; return a < b ? -PRIORITY_INCREMENT : PRIORITY_INCREMENT; } -export function dealsFirst(a, b) { +export function dealsFirst(a: any, b: any): number { if (a.adserverTargeting.hb_deal !== undefined && b.adserverTargeting.hb_deal === undefined) { return -1; } @@ -54,7 +57,7 @@ export function dealsFirst(a, b) { return 0; } -export const bidDesirabilityCompare = tiebreakCompare(priorityCompare, keyCompare((bid) => bid.desirability ?? bid.cpm)); +export const bidDesirabilityCompare = tiebreakCompare(priorityCompare, keyCompare((bid) => bid.desirability ?? bid.cpm)); // Sort comparator that orders bids by `.desirability` (falling back to cpm), highest first. export const highestDesirabilityFirst = reverseCompare(bidDesirabilityCompare); @@ -62,7 +65,7 @@ export const highestDesirabilityFirst = reverseCompare(bidDesirabilityCompare); // This function will get highest bid by `.desirability` (publisher `bidDesirabilityAdjustment` when set, else mirrors cpm via `adjustBids`); // ties use lowest `timeToRespond`, matching {@link getHighestCpm}. export const getHighestDesirability = maximum( - tiebreakCompare(bidDesirabilityCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond))) + tiebreakCompare(bidDesirabilityCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond))) ); // This function will get the oldest highest cpm value bid, in case of tie it will return the bid which came in first diff --git a/src/utils/winDimensions.js b/src/utils/winDimensions.ts similarity index 74% rename from src/utils/winDimensions.js rename to src/utils/winDimensions.ts index 83bc6a0f300..c824da9dfc1 100644 --- a/src/utils/winDimensions.js +++ b/src/utils/winDimensions.ts @@ -2,7 +2,20 @@ import { canAccessWindowTop, internal as utilsInternals } from '../utils.js'; import { CachedApiWrapper } from './cachedApiWrapper.js'; import { endLayoutEpoch, onLayoutEpochEnd, startLayoutEpoch } from './layoutEpoch.js'; -const winDimensions = new CachedApiWrapper( +export type WinDimensions = { + innerHeight: number; + innerWidth: number; + scrollX: number; + scrollY: number; + screen: { width: number; height: number }; + visualViewport: { width: number; height: number }; + document: { + documentElement: { clientWidth: number; clientHeight: number; scrollTop: number; scrollLeft: number }; + body: { scrollTop: number; scrollLeft: number; clientWidth: number; clientHeight: number }; + }; +}; + +const winDimensions = new CachedApiWrapper( () => { // called for every value read into the cache, including through an object obtained from // getWinDimensions() in an earlier epoch, so that each value is discarded when the epoch it was read in ends @@ -48,9 +61,9 @@ onLayoutEpochEnd(() => internal.winDimensions.reset()); /** * Dimensions and scroll offsets of the top window (or of this window, if the top one is not accessible). * Values are cached for the current layout epoch, together with element rects from the - * `boundingClientRect` library, so the two can be combined; see `layoutEpoch.js`. + * `boundingClientRect` library, so the two can be combined; see `layoutEpoch.ts`. */ -export function getWinDimensions() { +export function getWinDimensions(): WinDimensions { return internal.winDimensions.obj; } @@ -58,7 +71,7 @@ export function resetWinDimensions() { endLayoutEpoch(); } -export function getScreenOrientation(win) { +export function getScreenOrientation(win?: Window): 'portrait' | 'landscape' | undefined { const fallbackWin = win ?? (canAccessWindowTop() ? utilsInternals.getWindowTop() : utilsInternals.getWindowSelf()); const screenData = fallbackWin?.screen ?? utilsInternals.getWindowSelf()?.screen ?? window?.screen; const width = Number(screenData?.width);