@guardian/commercial-core 4.10.0 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/messenger/post-message.d.ts +1 -0
- package/dist/cjs/messenger/post-message.js +7 -0
- package/dist/cjs/messenger.d.ts +97 -0
- package/dist/cjs/messenger.js +281 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/messenger/post-message.d.ts +1 -0
- package/dist/esm/messenger/post-message.js +3 -0
- package/dist/esm/messenger.d.ts +97 -0
- package/dist/esm/messenger.js +274 -0
- package/package.json +1 -1
package/dist/cjs/index.d.ts
CHANGED
|
@@ -31,3 +31,5 @@ export { getSharedTargeting } from './targeting/shared';
|
|
|
31
31
|
export type { ViewportTargeting } from './targeting/viewport';
|
|
32
32
|
export { getViewportTargeting } from './targeting/viewport';
|
|
33
33
|
export { pickTargetingValues } from './targeting/pick-targeting-values';
|
|
34
|
+
export { init as initMessenger, type RegisterListener, type RegisterPersistentListener, type RespondProxy, } from './messenger';
|
|
35
|
+
export { postMessage } from './messenger/post-message';
|
package/dist/cjs/index.js
CHANGED
|
@@ -24,7 +24,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
24
24
|
return result;
|
|
25
25
|
};
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
-
exports.pickTargetingValues = exports.getViewportTargeting = exports.getSharedTargeting = exports.getSessionTargeting = exports.getPersonalisedTargeting = exports.getContentTargeting = exports.constants = exports.concatSizeMappings = exports.createAdSlot = exports.disabledAds = exports.buildAdsConfigWithConsent = exports.initTrackGpcSignal = exports.initTrackLabsContainer = exports.initTrackScrollDepth = exports.getPermutivePFPSegments = exports.getPermutiveSegments = exports.clearPermutiveSegments = exports.isAdBlockInUse = exports.isBreakpoint = exports.createAdSize = exports.slotSizeMappings = exports.getAdSize = exports.adSizes = exports.initCommercialMetrics = exports.bypassCommercialMetricsSampling = exports.EventTimer = exports.remarketing = exports.inizio = exports.twitter = exports.fbPixel = exports.permutive = exports.ias = void 0;
|
|
27
|
+
exports.postMessage = exports.initMessenger = exports.pickTargetingValues = exports.getViewportTargeting = exports.getSharedTargeting = exports.getSessionTargeting = exports.getPersonalisedTargeting = exports.getContentTargeting = exports.constants = exports.concatSizeMappings = exports.createAdSlot = exports.disabledAds = exports.buildAdsConfigWithConsent = exports.initTrackGpcSignal = exports.initTrackLabsContainer = exports.initTrackScrollDepth = exports.getPermutivePFPSegments = exports.getPermutiveSegments = exports.clearPermutiveSegments = exports.isAdBlockInUse = exports.isBreakpoint = exports.createAdSize = exports.slotSizeMappings = exports.getAdSize = exports.adSizes = exports.initCommercialMetrics = exports.bypassCommercialMetricsSampling = exports.EventTimer = exports.remarketing = exports.inizio = exports.twitter = exports.fbPixel = exports.permutive = exports.ias = void 0;
|
|
28
28
|
var ias_1 = require("./third-party-tags/ias");
|
|
29
29
|
Object.defineProperty(exports, "ias", { enumerable: true, get: function () { return ias_1.ias; } });
|
|
30
30
|
var permutive_1 = require("./third-party-tags/permutive");
|
|
@@ -80,3 +80,7 @@ var viewport_1 = require("./targeting/viewport");
|
|
|
80
80
|
Object.defineProperty(exports, "getViewportTargeting", { enumerable: true, get: function () { return viewport_1.getViewportTargeting; } });
|
|
81
81
|
var pick_targeting_values_1 = require("./targeting/pick-targeting-values");
|
|
82
82
|
Object.defineProperty(exports, "pickTargetingValues", { enumerable: true, get: function () { return pick_targeting_values_1.pickTargetingValues; } });
|
|
83
|
+
var messenger_1 = require("./messenger");
|
|
84
|
+
Object.defineProperty(exports, "initMessenger", { enumerable: true, get: function () { return messenger_1.init; } });
|
|
85
|
+
var post_message_1 = require("./messenger/post-message");
|
|
86
|
+
Object.defineProperty(exports, "postMessage", { enumerable: true, get: function () { return post_message_1.postMessage; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const postMessage: (message: Record<string, unknown>, target: MessageEventSource, targetOrigin?: string) => void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postMessage = void 0;
|
|
4
|
+
const postMessage = (message, target, targetOrigin = '*') => {
|
|
5
|
+
target.postMessage(JSON.stringify(message), { targetOrigin });
|
|
6
|
+
};
|
|
7
|
+
exports.postMessage = postMessage;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of iframe messages we accept
|
|
3
|
+
*/
|
|
4
|
+
declare type MessageType = 'background' | 'click' | 'disable-refresh' | 'get-page-targeting' | 'get-page-url' | 'get-styles' | 'measure-ad-load' | 'passback' | 'resize' | 'set-ad-height' | 'scroll' | 'type' | 'viewport';
|
|
5
|
+
/**
|
|
6
|
+
* Callbacks that can be registered to fire when receiving messages from an iframe
|
|
7
|
+
*/
|
|
8
|
+
declare type ListenerCallback = (
|
|
9
|
+
/**
|
|
10
|
+
* The data payload sent by an iframe, and has type `unknown` because we can't
|
|
11
|
+
* predict what the iframe will send. It is the responsibility of the callback
|
|
12
|
+
* to obtain a value of the desired type (or fail gracefully)
|
|
13
|
+
*/
|
|
14
|
+
specs: unknown | undefined,
|
|
15
|
+
/**
|
|
16
|
+
* Non-persistent callbacks can be chained together. This value is the return
|
|
17
|
+
* value of the previously fired callback in the chain. It is the responsibility
|
|
18
|
+
* of the current callback to either ignore it or use it / pass along
|
|
19
|
+
*/
|
|
20
|
+
ret: unknown,
|
|
21
|
+
/**
|
|
22
|
+
* Reference to the iframe that is the source of the message
|
|
23
|
+
*/
|
|
24
|
+
iframe?: HTMLIFrameElement) => unknown;
|
|
25
|
+
declare type RespondProxy = (error: {
|
|
26
|
+
message: string;
|
|
27
|
+
} | null, result: unknown) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Persistent callbacks that can be registered to fire when receiving messages from an iframe
|
|
30
|
+
*
|
|
31
|
+
* A persistent callback listener will be passed a RespondProxy function
|
|
32
|
+
*
|
|
33
|
+
* This function allows the listener to call respond (i.e. postMessage the originating iFrame) itself whenever it needs to
|
|
34
|
+
*
|
|
35
|
+
* This is useful for listeners such as viewport or scroll where the values change over time
|
|
36
|
+
*/
|
|
37
|
+
declare type PersistentListenerCallback = (respondProxy: RespondProxy, specs: unknown,
|
|
38
|
+
/**
|
|
39
|
+
* Reference to the iframe that is the source of the message
|
|
40
|
+
*/
|
|
41
|
+
iframe?: HTMLIFrameElement) => void;
|
|
42
|
+
declare type ListenerOptions = {
|
|
43
|
+
window?: WindowProxy;
|
|
44
|
+
};
|
|
45
|
+
declare type MessengerErrorHandler = (err: Error, features: Record<string, string>) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Types of functions to register a listener for a given type of iframe message
|
|
48
|
+
*/
|
|
49
|
+
declare type RegisterListener = (type: MessageType, callback: ListenerCallback, options?: ListenerOptions) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Types of functions to register a persistent listener for a given type of iframe message
|
|
52
|
+
*/
|
|
53
|
+
declare type RegisterPersistentListener = (type: MessageType, callback: PersistentListenerCallback, options?: ListenerOptions) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Types of functions to unregister a listener for a given type of iframe message
|
|
56
|
+
*
|
|
57
|
+
*/
|
|
58
|
+
declare type UnregisterListener = (type: MessageType, callback?: ListenerCallback, options?: ListenerOptions) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Register a listener for a given type of iframe message
|
|
61
|
+
*
|
|
62
|
+
* @param type The `type` of message to register against
|
|
63
|
+
* @param callback The listener callback to register that will receive messages of the given type
|
|
64
|
+
* @param options Options for the target window
|
|
65
|
+
*/
|
|
66
|
+
export declare const register: RegisterListener;
|
|
67
|
+
/**
|
|
68
|
+
* Register a persistent listener for a given type of iframe message
|
|
69
|
+
*
|
|
70
|
+
* @param type The `type` of message to register against
|
|
71
|
+
* @param callback The persistent listener callback to register that will receive messages of the given type
|
|
72
|
+
* @param options Options for the target window and whether the callback is persistent
|
|
73
|
+
*/
|
|
74
|
+
export declare const registerPersistentListener: RegisterPersistentListener;
|
|
75
|
+
/**
|
|
76
|
+
* Unregister a callback for a given type
|
|
77
|
+
*
|
|
78
|
+
* @param type The type of message to unregister against. An iframe will send
|
|
79
|
+
* messages annotated with the type
|
|
80
|
+
* @param callback Optionally include the original callback. If this is included
|
|
81
|
+
* for a persistent callback this function will be unregistered. If it's
|
|
82
|
+
* included for a non-persistent callback only the matching callback is removed,
|
|
83
|
+
* otherwise all callbacks for that type will be unregistered
|
|
84
|
+
* @param options Option for the target window
|
|
85
|
+
*/
|
|
86
|
+
export declare const unregister: UnregisterListener;
|
|
87
|
+
/**
|
|
88
|
+
* Initialize an array of listener callbacks in a batch
|
|
89
|
+
*
|
|
90
|
+
* @param listeners The listener registration functions
|
|
91
|
+
* @param persistentListeners The persistent listener registration functions
|
|
92
|
+
*/
|
|
93
|
+
export declare const init: (listeners: ((register: RegisterListener, errorHandler: MessengerErrorHandler) => void)[], persistentListeners: ((register: RegisterPersistentListener, errorHandler: MessengerErrorHandler) => void)[], errorHandler: MessengerErrorHandler) => void;
|
|
94
|
+
export declare const _: {
|
|
95
|
+
onMessage: (event: MessageEvent) => Promise<void>;
|
|
96
|
+
};
|
|
97
|
+
export type { RespondProxy, RegisterListener, RegisterPersistentListener, UnregisterListener, ListenerOptions, MessengerErrorHandler, };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._ = exports.init = exports.unregister = exports.registerPersistentListener = exports.register = void 0;
|
|
4
|
+
const post_message_1 = require("./messenger/post-message");
|
|
5
|
+
const LISTENERS = {};
|
|
6
|
+
let REGISTERED_LISTENERS = 0;
|
|
7
|
+
let reportError = () => {
|
|
8
|
+
// not set yet
|
|
9
|
+
};
|
|
10
|
+
const error405 = {
|
|
11
|
+
code: 405,
|
|
12
|
+
message: 'Service %% not implemented',
|
|
13
|
+
};
|
|
14
|
+
const error500 = {
|
|
15
|
+
code: 500,
|
|
16
|
+
message: 'Internal server error\n\n%%',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Determine if an unknown payload has the shape of a programmatic message
|
|
20
|
+
*
|
|
21
|
+
* @param payload The unknown message payload
|
|
22
|
+
*/
|
|
23
|
+
const isProgrammaticMessage = (payload) => {
|
|
24
|
+
const payloadToCheck = payload;
|
|
25
|
+
return (payloadToCheck.type === 'set-ad-height' &&
|
|
26
|
+
('id' in payloadToCheck.value || 'slotId' in payloadToCheck.value) &&
|
|
27
|
+
'height' in payloadToCheck.value);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Convert a legacy programmatic message to a standard message
|
|
31
|
+
*
|
|
32
|
+
* Note that this only applies to specific resize programmatic messages
|
|
33
|
+
* (these include specific width and height values)
|
|
34
|
+
*/
|
|
35
|
+
const toStandardMessage = (payload) => ({
|
|
36
|
+
id: 'aaaa0000-bb11-cc22-dd33-eeeeee444444',
|
|
37
|
+
type: 'resize',
|
|
38
|
+
iframeId: payload.value.id,
|
|
39
|
+
slotId: payload.value.slotId,
|
|
40
|
+
value: {
|
|
41
|
+
height: payload.value.height,
|
|
42
|
+
width: payload.value.width,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Retrieve a reference to the calling iFrame
|
|
47
|
+
*
|
|
48
|
+
* Attempts the following strategies to find the correct iframe:
|
|
49
|
+
* - using the slotId from the incoming message
|
|
50
|
+
* - using the iframeId from the incoming message
|
|
51
|
+
* - checking message event.source (i.e. window) against all page level iframe contentWindows
|
|
52
|
+
*
|
|
53
|
+
* Listeners can then use the iFrame to determine the slot making the postMessage call
|
|
54
|
+
*/
|
|
55
|
+
const getIframe = (message, messageEventSource) => {
|
|
56
|
+
if (message.slotId) {
|
|
57
|
+
const container = document.getElementById(`dfp-ad--${message.slotId}`);
|
|
58
|
+
return container?.querySelector('iframe') ?? undefined;
|
|
59
|
+
}
|
|
60
|
+
else if (message.iframeId) {
|
|
61
|
+
const el = document.getElementById(message.iframeId);
|
|
62
|
+
return el instanceof HTMLIFrameElement ? el : undefined;
|
|
63
|
+
}
|
|
64
|
+
else if (messageEventSource) {
|
|
65
|
+
const iframes = document.querySelectorAll('iframe');
|
|
66
|
+
return Array.from(iframes).find((iframe) => iframe.contentWindow === messageEventSource);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// Regex for testing validity of message ids
|
|
70
|
+
const validMessageRegex = /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/;
|
|
71
|
+
/**
|
|
72
|
+
* Narrow an `unknown` payload to the standard message format
|
|
73
|
+
*
|
|
74
|
+
* Until DFP provides a way for us to identify with 100% certainty our
|
|
75
|
+
* in-house creatives, we are left with doing some basic tests
|
|
76
|
+
* such as validating the anatomy of the payload and whitelisting
|
|
77
|
+
* event type
|
|
78
|
+
*/
|
|
79
|
+
const isValidPayload = (payload) => {
|
|
80
|
+
const payloadToCheck = payload;
|
|
81
|
+
return ('type' in payloadToCheck &&
|
|
82
|
+
'value' in payloadToCheck &&
|
|
83
|
+
'id' in payloadToCheck &&
|
|
84
|
+
payloadToCheck.type in LISTENERS &&
|
|
85
|
+
validMessageRegex.test(payloadToCheck.id));
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Cheap string formatting function
|
|
89
|
+
*
|
|
90
|
+
* @param error An object `{ code, message }`. `message` is a string where successive
|
|
91
|
+
* occurrences of %% will be replaced by the following arguments
|
|
92
|
+
* @param args Arguments that will replace %%
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* formatError({ message: "%%, you are so %%" }, "Regis", "lovely")
|
|
96
|
+
* => { message: "Regis, you are so lovely" }
|
|
97
|
+
*/
|
|
98
|
+
const formatError = (error, ...args) => args.reduce((e, arg) => {
|
|
99
|
+
e.message = e.message.replace('%%', arg);
|
|
100
|
+
return e;
|
|
101
|
+
}, error);
|
|
102
|
+
/**
|
|
103
|
+
* Convert a posted message to our StandardMessage format
|
|
104
|
+
*
|
|
105
|
+
* @param event The message event received on the window
|
|
106
|
+
* @returns A message with the `StandardMessage` format, or null if the conversion was unsuccessful
|
|
107
|
+
*/
|
|
108
|
+
const eventToStandardMessage = (event) => {
|
|
109
|
+
try {
|
|
110
|
+
// Currently all non-string messages are discarded here since parsing throws an error
|
|
111
|
+
// TODO Review whether this is the desired outcome
|
|
112
|
+
const data = JSON.parse(event.data);
|
|
113
|
+
const message = isProgrammaticMessage(data)
|
|
114
|
+
? toStandardMessage(data)
|
|
115
|
+
: data;
|
|
116
|
+
if (isValidPayload(message)) {
|
|
117
|
+
return message;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (ex) {
|
|
121
|
+
// Do nothing
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Respond to the original iframe with the result of calling the
|
|
126
|
+
* persistent listener / listener chain
|
|
127
|
+
*/
|
|
128
|
+
const respond = (id, target, error, result) => {
|
|
129
|
+
(0, post_message_1.postMessage)({
|
|
130
|
+
id,
|
|
131
|
+
error,
|
|
132
|
+
result,
|
|
133
|
+
}, target ?? window);
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Callback that is fired when an arbitrary message is received on the window
|
|
137
|
+
*
|
|
138
|
+
* @param event The message event received on the window
|
|
139
|
+
*/
|
|
140
|
+
const onMessage = async (event) => {
|
|
141
|
+
const message = eventToStandardMessage(event);
|
|
142
|
+
if (!message) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const listener = LISTENERS[message.type];
|
|
146
|
+
if (Array.isArray(listener) && listener.length) {
|
|
147
|
+
// Because any listener can have side-effects (by unregistering itself),
|
|
148
|
+
// we run the promise chain on a copy of the `LISTENERS` array.
|
|
149
|
+
// Hat tip @piuccio
|
|
150
|
+
const promise =
|
|
151
|
+
// We offer, but don't impose, the possibility that a listener returns
|
|
152
|
+
// a value that must be sent back to the calling frame. To do this,
|
|
153
|
+
// we pass the cumulated returned value as a second argument to each
|
|
154
|
+
// listener. Notice we don't try some clever way to compose the result
|
|
155
|
+
// value ourselves, this would only make the solution more complex.
|
|
156
|
+
// That means a listener can ignore the cumulated return value and
|
|
157
|
+
// return something else entirely—life is unfair.
|
|
158
|
+
// We don't know what each callack will be made of, we don't want to.
|
|
159
|
+
// And so we wrap each call in a promise chain, in case one drops the
|
|
160
|
+
// occasional fastdom bomb in the middle.
|
|
161
|
+
listener.reduce((func, listener) => func.then((ret) => {
|
|
162
|
+
const thisRet = listener(message.value, ret, getIframe(message, event.source));
|
|
163
|
+
return thisRet === undefined ? ret : thisRet;
|
|
164
|
+
}), Promise.resolve());
|
|
165
|
+
return promise
|
|
166
|
+
.then((response) => {
|
|
167
|
+
respond(message.id, event.source, null, response);
|
|
168
|
+
})
|
|
169
|
+
.catch((ex) => {
|
|
170
|
+
reportError(ex, {
|
|
171
|
+
feature: 'native-ads',
|
|
172
|
+
});
|
|
173
|
+
respond(message.id, event.source, formatError(error500, ex.toString()), null);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else if (typeof listener === 'function') {
|
|
177
|
+
// We found a persistent listener, to which we just delegate
|
|
178
|
+
// responsibility to write something. Anything. Really.
|
|
179
|
+
// The listener writes something by being given the `respond` function as the spec
|
|
180
|
+
listener(
|
|
181
|
+
// TODO change the arguments expected by persistent listeners to avoid this
|
|
182
|
+
(error, result) => respond(message.id, event.source, error, result), message.value, getIframe(message, event.source));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// If there is no routine attached to this event type, we just answer
|
|
186
|
+
// with an error code
|
|
187
|
+
respond(message.id, event.source, formatError(error405, message.type), null);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const on = (window) => {
|
|
191
|
+
window.addEventListener('message', (event) => void onMessage(event));
|
|
192
|
+
};
|
|
193
|
+
const off = (window) => {
|
|
194
|
+
window.removeEventListener('message', (event) => void onMessage(event));
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Register a listener for a given type of iframe message
|
|
198
|
+
*
|
|
199
|
+
* @param type The `type` of message to register against
|
|
200
|
+
* @param callback The listener callback to register that will receive messages of the given type
|
|
201
|
+
* @param options Options for the target window
|
|
202
|
+
*/
|
|
203
|
+
const register = (type, callback, options) => {
|
|
204
|
+
if (REGISTERED_LISTENERS === 0) {
|
|
205
|
+
on(options?.window ?? window);
|
|
206
|
+
}
|
|
207
|
+
const listeners = LISTENERS[type] ?? [];
|
|
208
|
+
if (Array.isArray(listeners) && !listeners.includes(callback)) {
|
|
209
|
+
LISTENERS[type] = [...listeners, callback];
|
|
210
|
+
REGISTERED_LISTENERS += 1;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
exports.register = register;
|
|
214
|
+
/**
|
|
215
|
+
* Register a persistent listener for a given type of iframe message
|
|
216
|
+
*
|
|
217
|
+
* @param type The `type` of message to register against
|
|
218
|
+
* @param callback The persistent listener callback to register that will receive messages of the given type
|
|
219
|
+
* @param options Options for the target window and whether the callback is persistent
|
|
220
|
+
*/
|
|
221
|
+
const registerPersistentListener = (type, callback, options) => {
|
|
222
|
+
if (REGISTERED_LISTENERS === 0) {
|
|
223
|
+
on(options?.window ?? window);
|
|
224
|
+
}
|
|
225
|
+
LISTENERS[type] = callback;
|
|
226
|
+
REGISTERED_LISTENERS += 1;
|
|
227
|
+
};
|
|
228
|
+
exports.registerPersistentListener = registerPersistentListener;
|
|
229
|
+
/**
|
|
230
|
+
* Unregister a callback for a given type
|
|
231
|
+
*
|
|
232
|
+
* @param type The type of message to unregister against. An iframe will send
|
|
233
|
+
* messages annotated with the type
|
|
234
|
+
* @param callback Optionally include the original callback. If this is included
|
|
235
|
+
* for a persistent callback this function will be unregistered. If it's
|
|
236
|
+
* included for a non-persistent callback only the matching callback is removed,
|
|
237
|
+
* otherwise all callbacks for that type will be unregistered
|
|
238
|
+
* @param options Option for the target window
|
|
239
|
+
*/
|
|
240
|
+
const unregister = (type, callback, options) => {
|
|
241
|
+
const listeners = LISTENERS[type];
|
|
242
|
+
if (listeners === undefined) {
|
|
243
|
+
throw new Error(formatError(error405, type).message);
|
|
244
|
+
}
|
|
245
|
+
else if (listeners === callback) {
|
|
246
|
+
LISTENERS[type] = undefined;
|
|
247
|
+
REGISTERED_LISTENERS -= 1;
|
|
248
|
+
}
|
|
249
|
+
else if (Array.isArray(listeners)) {
|
|
250
|
+
if (callback === undefined) {
|
|
251
|
+
LISTENERS[type] = [];
|
|
252
|
+
REGISTERED_LISTENERS -= listeners.length;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
LISTENERS[type] = listeners.filter((cb) => {
|
|
256
|
+
const callbacksEqual = cb === callback;
|
|
257
|
+
if (callbacksEqual) {
|
|
258
|
+
REGISTERED_LISTENERS -= 1;
|
|
259
|
+
}
|
|
260
|
+
return !callbacksEqual;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (REGISTERED_LISTENERS === 0) {
|
|
265
|
+
off(options?.window ?? window);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
exports.unregister = unregister;
|
|
269
|
+
/**
|
|
270
|
+
* Initialize an array of listener callbacks in a batch
|
|
271
|
+
*
|
|
272
|
+
* @param listeners The listener registration functions
|
|
273
|
+
* @param persistentListeners The persistent listener registration functions
|
|
274
|
+
*/
|
|
275
|
+
const init = (listeners, persistentListeners, errorHandler) => {
|
|
276
|
+
reportError = errorHandler;
|
|
277
|
+
listeners.forEach((moduleInit) => moduleInit(exports.register, errorHandler));
|
|
278
|
+
persistentListeners.forEach((moduleInit) => moduleInit(exports.registerPersistentListener, errorHandler));
|
|
279
|
+
};
|
|
280
|
+
exports.init = init;
|
|
281
|
+
exports._ = { onMessage };
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -31,3 +31,5 @@ export { getSharedTargeting } from './targeting/shared';
|
|
|
31
31
|
export type { ViewportTargeting } from './targeting/viewport';
|
|
32
32
|
export { getViewportTargeting } from './targeting/viewport';
|
|
33
33
|
export { pickTargetingValues } from './targeting/pick-targeting-values';
|
|
34
|
+
export { init as initMessenger, type RegisterListener, type RegisterPersistentListener, type RespondProxy, } from './messenger';
|
|
35
|
+
export { postMessage } from './messenger/post-message';
|
package/dist/esm/index.js
CHANGED
|
@@ -24,3 +24,5 @@ export { getSessionTargeting } from './targeting/session';
|
|
|
24
24
|
export { getSharedTargeting } from './targeting/shared';
|
|
25
25
|
export { getViewportTargeting } from './targeting/viewport';
|
|
26
26
|
export { pickTargetingValues } from './targeting/pick-targeting-values';
|
|
27
|
+
export { init as initMessenger, } from './messenger';
|
|
28
|
+
export { postMessage } from './messenger/post-message';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const postMessage: (message: Record<string, unknown>, target: MessageEventSource, targetOrigin?: string) => void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of iframe messages we accept
|
|
3
|
+
*/
|
|
4
|
+
declare type MessageType = 'background' | 'click' | 'disable-refresh' | 'get-page-targeting' | 'get-page-url' | 'get-styles' | 'measure-ad-load' | 'passback' | 'resize' | 'set-ad-height' | 'scroll' | 'type' | 'viewport';
|
|
5
|
+
/**
|
|
6
|
+
* Callbacks that can be registered to fire when receiving messages from an iframe
|
|
7
|
+
*/
|
|
8
|
+
declare type ListenerCallback = (
|
|
9
|
+
/**
|
|
10
|
+
* The data payload sent by an iframe, and has type `unknown` because we can't
|
|
11
|
+
* predict what the iframe will send. It is the responsibility of the callback
|
|
12
|
+
* to obtain a value of the desired type (or fail gracefully)
|
|
13
|
+
*/
|
|
14
|
+
specs: unknown | undefined,
|
|
15
|
+
/**
|
|
16
|
+
* Non-persistent callbacks can be chained together. This value is the return
|
|
17
|
+
* value of the previously fired callback in the chain. It is the responsibility
|
|
18
|
+
* of the current callback to either ignore it or use it / pass along
|
|
19
|
+
*/
|
|
20
|
+
ret: unknown,
|
|
21
|
+
/**
|
|
22
|
+
* Reference to the iframe that is the source of the message
|
|
23
|
+
*/
|
|
24
|
+
iframe?: HTMLIFrameElement) => unknown;
|
|
25
|
+
declare type RespondProxy = (error: {
|
|
26
|
+
message: string;
|
|
27
|
+
} | null, result: unknown) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Persistent callbacks that can be registered to fire when receiving messages from an iframe
|
|
30
|
+
*
|
|
31
|
+
* A persistent callback listener will be passed a RespondProxy function
|
|
32
|
+
*
|
|
33
|
+
* This function allows the listener to call respond (i.e. postMessage the originating iFrame) itself whenever it needs to
|
|
34
|
+
*
|
|
35
|
+
* This is useful for listeners such as viewport or scroll where the values change over time
|
|
36
|
+
*/
|
|
37
|
+
declare type PersistentListenerCallback = (respondProxy: RespondProxy, specs: unknown,
|
|
38
|
+
/**
|
|
39
|
+
* Reference to the iframe that is the source of the message
|
|
40
|
+
*/
|
|
41
|
+
iframe?: HTMLIFrameElement) => void;
|
|
42
|
+
declare type ListenerOptions = {
|
|
43
|
+
window?: WindowProxy;
|
|
44
|
+
};
|
|
45
|
+
declare type MessengerErrorHandler = (err: Error, features: Record<string, string>) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Types of functions to register a listener for a given type of iframe message
|
|
48
|
+
*/
|
|
49
|
+
declare type RegisterListener = (type: MessageType, callback: ListenerCallback, options?: ListenerOptions) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Types of functions to register a persistent listener for a given type of iframe message
|
|
52
|
+
*/
|
|
53
|
+
declare type RegisterPersistentListener = (type: MessageType, callback: PersistentListenerCallback, options?: ListenerOptions) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Types of functions to unregister a listener for a given type of iframe message
|
|
56
|
+
*
|
|
57
|
+
*/
|
|
58
|
+
declare type UnregisterListener = (type: MessageType, callback?: ListenerCallback, options?: ListenerOptions) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Register a listener for a given type of iframe message
|
|
61
|
+
*
|
|
62
|
+
* @param type The `type` of message to register against
|
|
63
|
+
* @param callback The listener callback to register that will receive messages of the given type
|
|
64
|
+
* @param options Options for the target window
|
|
65
|
+
*/
|
|
66
|
+
export declare const register: RegisterListener;
|
|
67
|
+
/**
|
|
68
|
+
* Register a persistent listener for a given type of iframe message
|
|
69
|
+
*
|
|
70
|
+
* @param type The `type` of message to register against
|
|
71
|
+
* @param callback The persistent listener callback to register that will receive messages of the given type
|
|
72
|
+
* @param options Options for the target window and whether the callback is persistent
|
|
73
|
+
*/
|
|
74
|
+
export declare const registerPersistentListener: RegisterPersistentListener;
|
|
75
|
+
/**
|
|
76
|
+
* Unregister a callback for a given type
|
|
77
|
+
*
|
|
78
|
+
* @param type The type of message to unregister against. An iframe will send
|
|
79
|
+
* messages annotated with the type
|
|
80
|
+
* @param callback Optionally include the original callback. If this is included
|
|
81
|
+
* for a persistent callback this function will be unregistered. If it's
|
|
82
|
+
* included for a non-persistent callback only the matching callback is removed,
|
|
83
|
+
* otherwise all callbacks for that type will be unregistered
|
|
84
|
+
* @param options Option for the target window
|
|
85
|
+
*/
|
|
86
|
+
export declare const unregister: UnregisterListener;
|
|
87
|
+
/**
|
|
88
|
+
* Initialize an array of listener callbacks in a batch
|
|
89
|
+
*
|
|
90
|
+
* @param listeners The listener registration functions
|
|
91
|
+
* @param persistentListeners The persistent listener registration functions
|
|
92
|
+
*/
|
|
93
|
+
export declare const init: (listeners: ((register: RegisterListener, errorHandler: MessengerErrorHandler) => void)[], persistentListeners: ((register: RegisterPersistentListener, errorHandler: MessengerErrorHandler) => void)[], errorHandler: MessengerErrorHandler) => void;
|
|
94
|
+
export declare const _: {
|
|
95
|
+
onMessage: (event: MessageEvent) => Promise<void>;
|
|
96
|
+
};
|
|
97
|
+
export type { RespondProxy, RegisterListener, RegisterPersistentListener, UnregisterListener, ListenerOptions, MessengerErrorHandler, };
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { postMessage } from './messenger/post-message';
|
|
2
|
+
const LISTENERS = {};
|
|
3
|
+
let REGISTERED_LISTENERS = 0;
|
|
4
|
+
let reportError = () => {
|
|
5
|
+
// not set yet
|
|
6
|
+
};
|
|
7
|
+
const error405 = {
|
|
8
|
+
code: 405,
|
|
9
|
+
message: 'Service %% not implemented',
|
|
10
|
+
};
|
|
11
|
+
const error500 = {
|
|
12
|
+
code: 500,
|
|
13
|
+
message: 'Internal server error\n\n%%',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Determine if an unknown payload has the shape of a programmatic message
|
|
17
|
+
*
|
|
18
|
+
* @param payload The unknown message payload
|
|
19
|
+
*/
|
|
20
|
+
const isProgrammaticMessage = (payload) => {
|
|
21
|
+
const payloadToCheck = payload;
|
|
22
|
+
return (payloadToCheck.type === 'set-ad-height' &&
|
|
23
|
+
('id' in payloadToCheck.value || 'slotId' in payloadToCheck.value) &&
|
|
24
|
+
'height' in payloadToCheck.value);
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Convert a legacy programmatic message to a standard message
|
|
28
|
+
*
|
|
29
|
+
* Note that this only applies to specific resize programmatic messages
|
|
30
|
+
* (these include specific width and height values)
|
|
31
|
+
*/
|
|
32
|
+
const toStandardMessage = (payload) => ({
|
|
33
|
+
id: 'aaaa0000-bb11-cc22-dd33-eeeeee444444',
|
|
34
|
+
type: 'resize',
|
|
35
|
+
iframeId: payload.value.id,
|
|
36
|
+
slotId: payload.value.slotId,
|
|
37
|
+
value: {
|
|
38
|
+
height: payload.value.height,
|
|
39
|
+
width: payload.value.width,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Retrieve a reference to the calling iFrame
|
|
44
|
+
*
|
|
45
|
+
* Attempts the following strategies to find the correct iframe:
|
|
46
|
+
* - using the slotId from the incoming message
|
|
47
|
+
* - using the iframeId from the incoming message
|
|
48
|
+
* - checking message event.source (i.e. window) against all page level iframe contentWindows
|
|
49
|
+
*
|
|
50
|
+
* Listeners can then use the iFrame to determine the slot making the postMessage call
|
|
51
|
+
*/
|
|
52
|
+
const getIframe = (message, messageEventSource) => {
|
|
53
|
+
if (message.slotId) {
|
|
54
|
+
const container = document.getElementById(`dfp-ad--${message.slotId}`);
|
|
55
|
+
return container?.querySelector('iframe') ?? undefined;
|
|
56
|
+
}
|
|
57
|
+
else if (message.iframeId) {
|
|
58
|
+
const el = document.getElementById(message.iframeId);
|
|
59
|
+
return el instanceof HTMLIFrameElement ? el : undefined;
|
|
60
|
+
}
|
|
61
|
+
else if (messageEventSource) {
|
|
62
|
+
const iframes = document.querySelectorAll('iframe');
|
|
63
|
+
return Array.from(iframes).find((iframe) => iframe.contentWindow === messageEventSource);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// Regex for testing validity of message ids
|
|
67
|
+
const validMessageRegex = /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/;
|
|
68
|
+
/**
|
|
69
|
+
* Narrow an `unknown` payload to the standard message format
|
|
70
|
+
*
|
|
71
|
+
* Until DFP provides a way for us to identify with 100% certainty our
|
|
72
|
+
* in-house creatives, we are left with doing some basic tests
|
|
73
|
+
* such as validating the anatomy of the payload and whitelisting
|
|
74
|
+
* event type
|
|
75
|
+
*/
|
|
76
|
+
const isValidPayload = (payload) => {
|
|
77
|
+
const payloadToCheck = payload;
|
|
78
|
+
return ('type' in payloadToCheck &&
|
|
79
|
+
'value' in payloadToCheck &&
|
|
80
|
+
'id' in payloadToCheck &&
|
|
81
|
+
payloadToCheck.type in LISTENERS &&
|
|
82
|
+
validMessageRegex.test(payloadToCheck.id));
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Cheap string formatting function
|
|
86
|
+
*
|
|
87
|
+
* @param error An object `{ code, message }`. `message` is a string where successive
|
|
88
|
+
* occurrences of %% will be replaced by the following arguments
|
|
89
|
+
* @param args Arguments that will replace %%
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* formatError({ message: "%%, you are so %%" }, "Regis", "lovely")
|
|
93
|
+
* => { message: "Regis, you are so lovely" }
|
|
94
|
+
*/
|
|
95
|
+
const formatError = (error, ...args) => args.reduce((e, arg) => {
|
|
96
|
+
e.message = e.message.replace('%%', arg);
|
|
97
|
+
return e;
|
|
98
|
+
}, error);
|
|
99
|
+
/**
|
|
100
|
+
* Convert a posted message to our StandardMessage format
|
|
101
|
+
*
|
|
102
|
+
* @param event The message event received on the window
|
|
103
|
+
* @returns A message with the `StandardMessage` format, or null if the conversion was unsuccessful
|
|
104
|
+
*/
|
|
105
|
+
const eventToStandardMessage = (event) => {
|
|
106
|
+
try {
|
|
107
|
+
// Currently all non-string messages are discarded here since parsing throws an error
|
|
108
|
+
// TODO Review whether this is the desired outcome
|
|
109
|
+
const data = JSON.parse(event.data);
|
|
110
|
+
const message = isProgrammaticMessage(data)
|
|
111
|
+
? toStandardMessage(data)
|
|
112
|
+
: data;
|
|
113
|
+
if (isValidPayload(message)) {
|
|
114
|
+
return message;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (ex) {
|
|
118
|
+
// Do nothing
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Respond to the original iframe with the result of calling the
|
|
123
|
+
* persistent listener / listener chain
|
|
124
|
+
*/
|
|
125
|
+
const respond = (id, target, error, result) => {
|
|
126
|
+
postMessage({
|
|
127
|
+
id,
|
|
128
|
+
error,
|
|
129
|
+
result,
|
|
130
|
+
}, target ?? window);
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Callback that is fired when an arbitrary message is received on the window
|
|
134
|
+
*
|
|
135
|
+
* @param event The message event received on the window
|
|
136
|
+
*/
|
|
137
|
+
const onMessage = async (event) => {
|
|
138
|
+
const message = eventToStandardMessage(event);
|
|
139
|
+
if (!message) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const listener = LISTENERS[message.type];
|
|
143
|
+
if (Array.isArray(listener) && listener.length) {
|
|
144
|
+
// Because any listener can have side-effects (by unregistering itself),
|
|
145
|
+
// we run the promise chain on a copy of the `LISTENERS` array.
|
|
146
|
+
// Hat tip @piuccio
|
|
147
|
+
const promise =
|
|
148
|
+
// We offer, but don't impose, the possibility that a listener returns
|
|
149
|
+
// a value that must be sent back to the calling frame. To do this,
|
|
150
|
+
// we pass the cumulated returned value as a second argument to each
|
|
151
|
+
// listener. Notice we don't try some clever way to compose the result
|
|
152
|
+
// value ourselves, this would only make the solution more complex.
|
|
153
|
+
// That means a listener can ignore the cumulated return value and
|
|
154
|
+
// return something else entirely—life is unfair.
|
|
155
|
+
// We don't know what each callack will be made of, we don't want to.
|
|
156
|
+
// And so we wrap each call in a promise chain, in case one drops the
|
|
157
|
+
// occasional fastdom bomb in the middle.
|
|
158
|
+
listener.reduce((func, listener) => func.then((ret) => {
|
|
159
|
+
const thisRet = listener(message.value, ret, getIframe(message, event.source));
|
|
160
|
+
return thisRet === undefined ? ret : thisRet;
|
|
161
|
+
}), Promise.resolve());
|
|
162
|
+
return promise
|
|
163
|
+
.then((response) => {
|
|
164
|
+
respond(message.id, event.source, null, response);
|
|
165
|
+
})
|
|
166
|
+
.catch((ex) => {
|
|
167
|
+
reportError(ex, {
|
|
168
|
+
feature: 'native-ads',
|
|
169
|
+
});
|
|
170
|
+
respond(message.id, event.source, formatError(error500, ex.toString()), null);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else if (typeof listener === 'function') {
|
|
174
|
+
// We found a persistent listener, to which we just delegate
|
|
175
|
+
// responsibility to write something. Anything. Really.
|
|
176
|
+
// The listener writes something by being given the `respond` function as the spec
|
|
177
|
+
listener(
|
|
178
|
+
// TODO change the arguments expected by persistent listeners to avoid this
|
|
179
|
+
(error, result) => respond(message.id, event.source, error, result), message.value, getIframe(message, event.source));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// If there is no routine attached to this event type, we just answer
|
|
183
|
+
// with an error code
|
|
184
|
+
respond(message.id, event.source, formatError(error405, message.type), null);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const on = (window) => {
|
|
188
|
+
window.addEventListener('message', (event) => void onMessage(event));
|
|
189
|
+
};
|
|
190
|
+
const off = (window) => {
|
|
191
|
+
window.removeEventListener('message', (event) => void onMessage(event));
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Register a listener for a given type of iframe message
|
|
195
|
+
*
|
|
196
|
+
* @param type The `type` of message to register against
|
|
197
|
+
* @param callback The listener callback to register that will receive messages of the given type
|
|
198
|
+
* @param options Options for the target window
|
|
199
|
+
*/
|
|
200
|
+
export const register = (type, callback, options) => {
|
|
201
|
+
if (REGISTERED_LISTENERS === 0) {
|
|
202
|
+
on(options?.window ?? window);
|
|
203
|
+
}
|
|
204
|
+
const listeners = LISTENERS[type] ?? [];
|
|
205
|
+
if (Array.isArray(listeners) && !listeners.includes(callback)) {
|
|
206
|
+
LISTENERS[type] = [...listeners, callback];
|
|
207
|
+
REGISTERED_LISTENERS += 1;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Register a persistent listener for a given type of iframe message
|
|
212
|
+
*
|
|
213
|
+
* @param type The `type` of message to register against
|
|
214
|
+
* @param callback The persistent listener callback to register that will receive messages of the given type
|
|
215
|
+
* @param options Options for the target window and whether the callback is persistent
|
|
216
|
+
*/
|
|
217
|
+
export const registerPersistentListener = (type, callback, options) => {
|
|
218
|
+
if (REGISTERED_LISTENERS === 0) {
|
|
219
|
+
on(options?.window ?? window);
|
|
220
|
+
}
|
|
221
|
+
LISTENERS[type] = callback;
|
|
222
|
+
REGISTERED_LISTENERS += 1;
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Unregister a callback for a given type
|
|
226
|
+
*
|
|
227
|
+
* @param type The type of message to unregister against. An iframe will send
|
|
228
|
+
* messages annotated with the type
|
|
229
|
+
* @param callback Optionally include the original callback. If this is included
|
|
230
|
+
* for a persistent callback this function will be unregistered. If it's
|
|
231
|
+
* included for a non-persistent callback only the matching callback is removed,
|
|
232
|
+
* otherwise all callbacks for that type will be unregistered
|
|
233
|
+
* @param options Option for the target window
|
|
234
|
+
*/
|
|
235
|
+
export const unregister = (type, callback, options) => {
|
|
236
|
+
const listeners = LISTENERS[type];
|
|
237
|
+
if (listeners === undefined) {
|
|
238
|
+
throw new Error(formatError(error405, type).message);
|
|
239
|
+
}
|
|
240
|
+
else if (listeners === callback) {
|
|
241
|
+
LISTENERS[type] = undefined;
|
|
242
|
+
REGISTERED_LISTENERS -= 1;
|
|
243
|
+
}
|
|
244
|
+
else if (Array.isArray(listeners)) {
|
|
245
|
+
if (callback === undefined) {
|
|
246
|
+
LISTENERS[type] = [];
|
|
247
|
+
REGISTERED_LISTENERS -= listeners.length;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
LISTENERS[type] = listeners.filter((cb) => {
|
|
251
|
+
const callbacksEqual = cb === callback;
|
|
252
|
+
if (callbacksEqual) {
|
|
253
|
+
REGISTERED_LISTENERS -= 1;
|
|
254
|
+
}
|
|
255
|
+
return !callbacksEqual;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (REGISTERED_LISTENERS === 0) {
|
|
260
|
+
off(options?.window ?? window);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Initialize an array of listener callbacks in a batch
|
|
265
|
+
*
|
|
266
|
+
* @param listeners The listener registration functions
|
|
267
|
+
* @param persistentListeners The persistent listener registration functions
|
|
268
|
+
*/
|
|
269
|
+
export const init = (listeners, persistentListeners, errorHandler) => {
|
|
270
|
+
reportError = errorHandler;
|
|
271
|
+
listeners.forEach((moduleInit) => moduleInit(register, errorHandler));
|
|
272
|
+
persistentListeners.forEach((moduleInit) => moduleInit(registerPersistentListener, errorHandler));
|
|
273
|
+
};
|
|
274
|
+
export const _ = { onMessage };
|