• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prebid / Prebid.js / #298

29 May 2025 11:21AM UTC coverage: 82.464% (-7.7%) from 90.144%
#298

push

travis-ci

prebidjs-release
Prebid 9.45.0 release

12606 of 17518 branches covered (71.96%)

18622 of 22582 relevant lines covered (82.46%)

157.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.93
/libraries/cmp/cmpClient.js
1
import {PbPromise} from '../../src/utils/promise.js';
2

3
/**
4
 * @typedef {function} CMPClient
5
 *
6
 * @param {{}} params CMP parameters. Currently this is a subset of {command, callback, parameter, version}.
7
 * @param {boolean} once if true, discard cross-frame event listeners once a reply message is received.
8
 * @returns {Promise<*>} a promise to the API's "result" - see the `mode` argument to `cmpClient` on how that's determined.
9
 * @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required)
10
 * @property {() => void} close close the client; currently, this just stops listening for cross-frame messages.
11
 */
12

13
export const MODE_MIXED = 0;
1✔
14
export const MODE_RETURN = 1;
1✔
15
export const MODE_CALLBACK = 2;
1✔
16

17
/**
18
 * Returns a client function that can interface with a CMP regardless of where it's located.
19
 *
20
 * @param {object} obj
21
 * @param obj.apiName name of the CMP api, e.g. "__gpp"
22
 * @param [obj.apiVersion] CMP API version
23
 * @param [obj.apiArgs] names of the arguments taken by the api function, in order.
24
 * @param [obj.callbackArgs] names of the cross-frame response payload properties that should be passed as callback arguments, in order
25
 * @param [obj.mode] controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved.
26
 *
27
 * The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these
28
 * cases "subscriptions" and "one-shot calls" respectively:
29
 *
30
 * With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback
31
 * is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and
32
 * left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's
33
 * return value when it's directly accessible, or with the result from the first (and, presumably, the only)
34
 * cross-frame reply when it's not;
35
 *
36
 * With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined
37
 * when cross-frame;
38
 *
39
 * With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead,
40
 * it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will
41
 * automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve
42
 * the returned promise. Subscriptions are treated in the same way as MODE_MIXED.
43
 *
44
 * @param win
45
 * @returns {CMPClient} CMP invocation function (or null if no CMP was found).
46
 */
47
export function cmpClient(
48
  {
49
    apiName,
50
    apiVersion,
51
    apiArgs = ['command', 'callback', 'parameter', 'version'],
×
52
    callbackArgs = ['returnValue', 'success'],
20✔
53
    mode = MODE_MIXED,
20✔
54
  },
55
  win = window
20✔
56
) {
57
  const cmpCallbacks = {};
20✔
58
  const callName = `${apiName}Call`;
20✔
59
  const cmpDataPkgName = `${apiName}Return`;
20✔
60

61
  function handleMessage(event) {
62
    const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data;
×
63
    if (json?.[cmpDataPkgName]?.callId) {
×
64
      const payload = json[cmpDataPkgName];
×
65

66
      if (cmpCallbacks.hasOwnProperty(payload.callId)) {
×
67
        cmpCallbacks[payload.callId](...callbackArgs.map(name => payload[name]));
×
68
      }
69
    }
70
  }
71

72
  function findCMP() {
73
    let f = win;
20✔
74
    let cmpFrame;
75
    let isDirect = false;
20✔
76
    while (f != null) {
20✔
77
      try {
40✔
78
        if (typeof f[apiName] === 'function') {
40!
79
          cmpFrame = f;
×
80
          isDirect = true;
×
81
          break;
×
82
        }
83
      } catch (e) {
84
      }
85

86
      // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env
87
      try {
40✔
88
        if (f.frames[`${apiName}Locator`]) {
40!
89
          cmpFrame = f;
×
90
          break;
×
91
        }
92
      } catch (e) {
93
      }
94

95
      if (f === win.top) break;
40✔
96
      f = f.parent;
20✔
97
    }
98

99
    return [
20✔
100
      cmpFrame,
101
      isDirect
102
    ];
103
  }
104

105
  const [cmpFrame, isDirect] = findCMP();
20✔
106

107
  if (!cmpFrame) {
20!
108
    return;
20✔
109
  }
110

111
  function resolveParams(params) {
112
    params = Object.assign({version: apiVersion}, params);
×
113
    return apiArgs.map(arg => [arg, params[arg]])
×
114
  }
115

116
  function wrapCallback(callback, resolve, reject, preamble) {
117
    const haveCb = typeof callback === 'function';
×
118

119
    return function (result, success) {
×
120
      preamble && preamble();
×
121
      if (mode !== MODE_RETURN) {
×
122
        const resolver = success == null || success ? resolve : reject;
×
123
        resolver(haveCb ? undefined : result);
×
124
      }
125
      haveCb && callback.apply(this, arguments);
×
126
    }
127
  }
128

129
  let client;
130

131
  if (isDirect) {
×
132
    client = function invokeCMPDirect(params = {}) {
×
133
      return new PbPromise((resolve, reject) => {
×
134
        const ret = cmpFrame[apiName](...resolveParams({
×
135
          ...params,
136
          callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined,
×
137
        }).map(([_, val]) => val));
×
138
        if (mode === MODE_RETURN || (params.callback == null && mode === MODE_MIXED)) {
×
139
          resolve(ret);
×
140
        }
141
      });
142
    };
143
  } else {
144
    win.addEventListener('message', handleMessage, false);
×
145

146
    client = function invokeCMPFrame(params, once = false) {
×
147
      return new PbPromise((resolve, reject) => {
×
148
        // call CMP via postMessage
149
        const callId = Math.random().toString();
×
150
        const msg = {
×
151
          [callName]: {
152
            ...Object.fromEntries(resolveParams(params).filter(([param]) => param !== 'callback')),
×
153
            callId: callId
154
          }
155
        };
156

157
        cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, (once || params?.callback == null) && (() => { delete cmpCallbacks[callId] }));
×
158
        cmpFrame.postMessage(msg, '*');
×
159
        if (mode === MODE_RETURN) resolve();
×
160
      });
161
    };
162
  }
163
  return Object.assign(client, {
×
164
    isDirect,
165
    close() {
166
      !isDirect && win.removeEventListener('message', handleMessage);
×
167
    }
168
  })
169
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc