Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: start yielding control of the main thread #12025

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d0ceb3b
Stop using greedypromise
dgirardi Jul 16, 2024
5d743f3
async tests: adxc, aso, blasto, criteo
dgirardi Jul 16, 2024
8e07c50
async tests: improvedigital
dgirardi Jul 16, 2024
11c67b9
use async
dgirardi Jul 16, 2024
f924418
asyn tests: openx, pbs, pulsepoint
dgirardi Jul 16, 2024
ecfc15b
async tests: rubicon, silvermob, tpmn, trafficgate
dgirardi Jul 16, 2024
7d01462
async tests: userId
dgirardi Jul 16, 2024
dd9efd6
async tests: geolocation
dgirardi Jul 16, 2024
5838672
async tests: auctions
dgirardi Jul 17, 2024
faa6a7b
refactor gpp
dgirardi Jul 22, 2024
0f93ada
refactor tcf
dgirardi Jul 22, 2024
152c549
wip: pbjs_api_spec
dgirardi Jul 22, 2024
ed59004
async tests: pbjs_api_spec
dgirardi Jul 23, 2024
6bd60a9
async tests: stragglers
dgirardi Jul 23, 2024
191f49e
async tests: passing
dgirardi Jul 23, 2024
ba5e2f8
Merge branch 'master' into stop-hoarding
dgirardi Jul 23, 2024
cdd2fb0
Use GreedyPromise instead of async
dgirardi Jul 23, 2024
0afcf34
rename timeout to delay
dgirardi Jul 23, 2024
86a2008
Reinstate GreedyPromise as a library
dgirardi Jul 23, 2024
77988bd
async tests: pbjs_api
dgirardi Jul 23, 2024
4fc94b0
rename GreedyPromise to PbPromise
dgirardi Jul 23, 2024
03de69e
reset GPP data on each test
dgirardi Jul 23, 2024
e8e1d14
dupe checker fooled by whitespace?
dgirardi Jul 23, 2024
3928256
Remove sync version of test utils
dgirardi Jul 23, 2024
ef753cf
Fix cmUtils timeout bug
dgirardi Jul 24, 2024
b38cfd3
Extract consentManagement config parsing
dgirardi Jul 25, 2024
b9c8473
Extract consent module config logic
dgirardi Jul 25, 2024
9c8bdde
fix greedy setTimeout
dgirardi Jul 25, 2024
bd79982
Merge branch 'master' into stop-hoarding
dgirardi Jul 31, 2024
0365855
fix 8podAnalytics tests
dgirardi Jul 31, 2024
c574fee
Merge branch 'master' into stop-hoarding
dgirardi Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion features.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
"NATIVE",
"VIDEO",
"UID2_CSTG"
"UID2_CSTG",
"GREEDY"
]
2 changes: 1 addition & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ gulp.task('build-bundle-dev', gulp.series('build-creative-dev', makeDevpackPkg(s
gulp.task('build-bundle-prod', gulp.series('build-creative-prod', makeWebpackPkg(standaloneDebuggingConfig), makeWebpackPkg(), gulpBundle.bind(null, false)));
// build-bundle-verbose - prod bundle except names and comments are preserved. Use this to see the effects
// of dead code elimination.
gulp.task('build-bundle-verbose', gulp.series('build-creative-dev', makeWebpackPkg(makeVerbose(standaloneDebuggingConfig)), makeWebpackPkg(makeVerbose()), gulpBundle.bind(null, true)));
gulp.task('build-bundle-verbose', gulp.series('build-creative-dev', makeWebpackPkg(makeVerbose(standaloneDebuggingConfig)), makeWebpackPkg(makeVerbose()), gulpBundle.bind(null, false)));

// public tasks (dependencies are needed for each task since they can be ran on their own)
gulp.task('test-only', test);
Expand Down
6 changes: 3 additions & 3 deletions libraries/cmp/cmpClient.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {GreedyPromise} from '../../src/utils/promise.js';
import {PbPromise} from '../../src/utils/promise.js';

/**
* @typedef {function} CMPClient
Expand Down Expand Up @@ -130,7 +130,7 @@ export function cmpClient(

if (isDirect) {
client = function invokeCMPDirect(params = {}) {
return new GreedyPromise((resolve, reject) => {
return new PbPromise((resolve, reject) => {
const ret = cmpFrame[apiName](...resolveParams({
...params,
callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined,
Expand All @@ -144,7 +144,7 @@ export function cmpClient(
win.addEventListener('message', handleMessage, false);

client = function invokeCMPFrame(params, once = false) {
return new GreedyPromise((resolve, reject) => {
return new PbPromise((resolve, reject) => {
// call CMP via postMessage
const callId = Math.random().toString();
const msg = {
Expand Down
205 changes: 176 additions & 29 deletions libraries/consentManagement/cmUtils.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,185 @@
import {timedAuctionHook} from '../../src/utils/perfMetrics.js';
import {logError, logInfo, logWarn} from '../../src/utils.js';

export function consentManagementHook(name, getConsent, loadConsentData) {
function loadIfMissing(cb) {
if (getConsent()) {
logInfo('User consent information already known. Pulling internally stored information...');
// eslint-disable-next-line standard/no-callback-literal
cb(false);
} else {
loadConsentData(cb);
}
}
import {isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../../src/utils.js';
import {ConsentHandler} from '../../src/consentHandler.js';
import {getGlobal} from '../../src/prebidGlobal.js';
import {PbPromise} from '../../src/utils/promise.js';
import {buildActivityParams} from '../../src/activities/params.js';

export function consentManagementHook(name, loadConsentData) {
const SEEN = new WeakSet();
return timedAuctionHook(name, function requestBidsHook(fn, reqBidsConfigObj) {
loadIfMissing(function (shouldCancelAuction, errMsg, ...extraArgs) {
if (errMsg) {
let log = logWarn;
if (shouldCancelAuction) {
log = logError;
errMsg = `${errMsg} Canceling auction as per consentManagement config.`;
}
log(errMsg, ...extraArgs);
return loadConsentData().then(({consentData, error}) => {
if (error && (!consentData || !SEEN.has(error))) {
SEEN.add(error);
logWarn(error.message, ...(error.args || []));
} else if (consentData) {
logInfo(`${name.toUpperCase()}: User consent information already known. Pulling internally stored information...`);
}

if (shouldCancelAuction) {
fn.stopTiming();
if (typeof reqBidsConfigObj.bidsBackHandler === 'function') {
reqBidsConfigObj.bidsBackHandler();
} else {
logError('Error executing bidsBackHandler');
}
fn.call(this, reqBidsConfigObj);
}).catch((error) => {
logError(`${error?.message} Canceling auction as per consentManagement config.`, ...(error?.args || []));
fn.stopTiming();
if (typeof reqBidsConfigObj.bidsBackHandler === 'function') {
reqBidsConfigObj.bidsBackHandler();
} else {
fn.call(this, reqBidsConfigObj);
logError('Error executing bidsBackHandler');
}
});
});
}

/**
*
* @typedef {Function} CmpLookupFn CMP lookup function. Should set up communication and keep consent data updated
* through consent data handlers' `setConsentData`.
* @param {SetProvisionalConsent} setProvisionalConsent optionally, the function can call this with provisional consent
* data, which will be used if the lookup times out before "proper" consent data can be retrieved.
* @returns {Promise<{void}>} a promise that resolves when the auction should be continued, or rejects if it should be canceled.
*
* @typedef {Function} SetProvisionalConsent
* @param {*} provisionalConsent
* @returns {void}
*/

/**
* Look up consent data from CMP or config.
*
* @param {Object} options
* @param {String} options.name e.g. 'GPP'. Used only for log messages.
* @param {ConsentHandler} options.consentDataHandler consent data handler object (from src/consentHandler)
* @param {CmpLookupFn} options.setupCmp
* @param {Number?} options.cmpTimeout timeout (in ms) after which the auction should continue without consent data.
* @param {Number?} options.actionTimeout timeout (in ms) from when provisional consent is available to when the auction should continue with it
* @param {() => {}} options.getNullConsent consent data to use on timeout
* @returns {Promise<{error: Error, consentData: {}}>}
*/
export function lookupConsentData(
{
name,
consentDataHandler,
setupCmp,
cmpTimeout,
actionTimeout,
getNullConsent
}
) {
consentDataHandler.enable();
let timeoutHandle;

return new Promise((resolve, reject) => {
let provisionalConsent;
let cmpLoaded = false;

function setProvisionalConsent(consentData) {
provisionalConsent = consentData;
if (!cmpLoaded) {
cmpLoaded = true;
actionTimeout != null && resetTimeout(actionTimeout);
}
}

function resetTimeout(timeout) {
if (timeoutHandle != null) clearTimeout(timeoutHandle);
if (timeout != null) {
timeoutHandle = setTimeout(() => {
const consentData = consentDataHandler.getConsentData() ?? (cmpLoaded ? provisionalConsent : getNullConsent());
const message = `timeout waiting for ${cmpLoaded ? 'user action on CMP' : 'CMP to load'}`;
consentDataHandler.setConsentData(consentData);
resolve({consentData, error: new Error(`${name} ${message}`)});
}, timeout);
} else {
timeoutHandle = null;
}
}
setupCmp(setProvisionalConsent)
.then(() => resolve({consentData: consentDataHandler.getConsentData()}), reject);
cmpTimeout != null && resetTimeout(cmpTimeout);
}).finally(() => {
timeoutHandle && clearTimeout(timeoutHandle);
}).catch((e) => {
consentDataHandler.setConsentData(null);
throw e;
});
}

export function configParser(
{
namespace,
displayName,
consentDataHandler,
parseConsentData,
getNullConsent,
cmpHandlers,
DEFAULT_CMP = 'iab',
DEFAULT_CONSENT_TIMEOUT = 10000
} = {}
) {
function msg(message) {
return `consentManagement.${namespace} ${message}`;
}
let requestBidsHook, consentDataLoaded, staticConsentData;

return function getConsentConfig(config) {
config = config?.[namespace];
if (!config || typeof config !== 'object') {
logWarn(msg(`config not defined, exiting consent manager module`));
return {};
}
let cmpHandler;
if (isStr(config.cmpApi)) {
cmpHandler = config.cmpApi;
} else {
cmpHandler = DEFAULT_CMP;
logInfo(msg(`config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`));
}
let cmpTimeout;
if (isNumber(config.timeout)) {
cmpTimeout = config.timeout;
} else {
cmpTimeout = DEFAULT_CONSENT_TIMEOUT;
logInfo(msg(`config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`));
}
const actionTimeout = isNumber(config.actionTimeout) ? config.actionTimeout : null;
let setupCmp;
if (cmpHandler === 'static') {
if (isPlainObject(config.consentData)) {
staticConsentData = config.consentData;
cmpTimeout = null;
setupCmp = () => new PbPromise(resolve => resolve(consentDataHandler.setConsentData(parseConsentData(staticConsentData))))
} else {
logError(msg(`config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`));
}
} else if (!cmpHandlers.hasOwnProperty(cmpHandler)) {
consentDataHandler.setConsentData(null);
logWarn(`${displayName} CMP framework (${cmpHandler}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
setupCmp = () => PbPromise.resolve();
} else {
setupCmp = cmpHandlers[cmpHandler];
}
consentDataLoaded = lookupConsentData({
name: displayName,
consentDataHandler,
setupCmp,
cmpTimeout,
actionTimeout,
getNullConsent,
})
const loadConsentData = () => consentDataLoaded.then(({error}) => ({error, consentData: consentDataHandler.getConsentData()}))
if (requestBidsHook == null) {
requestBidsHook = consentManagementHook(namespace, () => consentDataLoaded);
getGlobal().requestBids.before(requestBidsHook, 50);
buildActivityParams.before((next, params) => {
return next(Object.assign({[`${namespace}Consent`]: consentDataHandler.getConsentData()}, params));
});
}
logInfo(`${displayName} consentManagement module has been activated...`)
return {
cmpHandler,
cmpTimeout,
actionTimeout,
staticConsentData,
loadConsentData,
requestBidsHook
}
}
}
116 changes: 116 additions & 0 deletions libraries/greedy/greedyPromise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const SUCCESS = 0;
const FAIL = 1;

/**
* A version of Promise that runs callbacks synchronously when it can (i.e. after it's been fulfilled or rejected).
*/
export class GreedyPromise {
#result;
#callbacks;

constructor(resolver) {
if (typeof resolver !== 'function') {
throw new Error('resolver not a function');
}
const result = [];
const callbacks = [];
let [resolve, reject] = [SUCCESS, FAIL].map((type) => {
return function (value) {
if (type === SUCCESS && typeof value?.then === 'function') {
value.then(resolve, reject);
} else if (!result.length) {
result.push(type, value);
while (callbacks.length) callbacks.shift()();
}
}
});
try {
resolver(resolve, reject);
} catch (e) {
reject(e);
}
this.#result = result;
this.#callbacks = callbacks;
}

then(onSuccess, onError) {
const result = this.#result;
return new this.constructor((resolve, reject) => {
const continuation = () => {
let value = result[1];
let [handler, resolveFn] = result[0] === SUCCESS ? [onSuccess, resolve] : [onError, reject];
if (typeof handler === 'function') {
try {
value = handler(value);
} catch (e) {
reject(e);
return;
}
resolveFn = resolve;
}
resolveFn(value);
}
result.length ? continuation() : this.#callbacks.push(continuation);
});
}

catch(onError) {
return this.then(null, onError);
}

finally(onFinally) {
let val;
return this.then(
(v) => { val = v; return onFinally(); },
(e) => { val = this.constructor.reject(e); return onFinally() }
).then(() => val);
}

static #collect(promises, collector, done) {
let cnt = promises.length;
function clt() {
collector.apply(this, arguments);
if (--cnt <= 0 && done) done();
}
promises.length === 0 && done ? done() : promises.forEach((p, i) => this.resolve(p).then(
(val) => clt(true, val, i),
(err) => clt(false, err, i)
));
}

static race(promises) {
return new this((resolve, reject) => {
this.#collect(promises, (success, result) => success ? resolve(result) : reject(result));
})
}

static all(promises) {
return new this((resolve, reject) => {
let res = [];
this.#collect(promises, (success, val, i) => success ? res[i] = val : reject(val), () => resolve(res));
})
}

static allSettled(promises) {
return new this((resolve) => {
let res = [];
this.#collect(promises, (success, val, i) => res[i] = success ? {status: 'fulfilled', value: val} : {status: 'rejected', reason: val}, () => resolve(res))
})
}

static resolve(value) {
return new this(resolve => resolve(value))
}

static reject(error) {
return new this((resolve, reject) => reject(error))
}
}

export function greedySetTimeout(fn, delayMs = 0) {
if (delayMs > 0) {
return setTimeout(fn, delayMs)
} else {
fn()
}
}
Loading