@dash0/sdk-web
Version:
Dash0's Web SDK to collect telemetry from end-users' web browsers
158 lines (140 loc) • 5.35 kB
text/typescript
const WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY = "__dash0OriginalFunctions";
export type EventListenerOptionsOrUseCapture =
| boolean
| {
capture?: boolean;
};
// Asynchronous function wrapping: The process of wrapping a listener which goes into one function, e.g.
//
// - EventTarget#addEventListener
// - EventEmitter#on
//
// and is removed via another function, e.g.
//
// - EventTarget#removeEventListener
// - EventEmitter#off
//
// What is complicated about this, is that these methods identify registered listeners by function reference.
// When we wrap a function, we naturally change the reference. We must therefore keep track of which
// original function belongs to what wrapped function.
//
// This file provides helpers that help in the typical cases. It is removed from all browser specific APIs
// in order to allow simple unit test execution.
//
// Note that this file follows the behavior outlined in DOM specification. Among others, this means that it is not
// possible to register the same listener twice.
// http://dom.spec.whatwg.org
export function addWrappedFunction(
storageTarget: EventTarget,
wrappedFunction: EventListenerOrEventListenerObject,
valuesForEqualityCheck: any[]
): EventListenerOrEventListenerObject {
if (!storageTarget) return wrappedFunction;
const storage = ((storageTarget as any)[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY] =
(storageTarget as any)[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY] || []);
const index = findInStorage(storageTarget, valuesForEqualityCheck);
if (index !== -1) {
// already registered. Do not allow re-registration
return storage[index].wrappedFunction;
}
storage.push({
wrappedFunction,
valuesForEqualityCheck,
});
return wrappedFunction;
}
function findInStorage(storageTarget: EventTarget, valuesForEqualityCheck: any[]): number {
const storage = (storageTarget as any)[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY];
for (let i = 0; i < storage.length; i++) {
const storageItem = storage[i];
if (matchesEqualityCheck(storageItem.valuesForEqualityCheck, valuesForEqualityCheck)) {
return i;
}
}
return -1;
}
export function popWrappedFunction(
storageTarget: EventTarget,
valuesForEqualityCheck: any[],
fallback?: EventListenerOrEventListenerObject
): EventListenerOrEventListenerObject | undefined {
const storage = (storageTarget as any)?.[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY];
if (storage == null) {
return fallback;
}
const index = findInStorage(storageTarget, valuesForEqualityCheck);
if (index === -1) {
return fallback;
}
const storageItem = storage[index];
storage.splice(index, 1);
return storageItem.wrappedFunction;
}
function matchesEqualityCheck(valuesForEqualityCheckA: any, valuesForEqualityCheckB: any) {
if (valuesForEqualityCheckA.length !== valuesForEqualityCheckB.length) {
return false;
}
for (let i = 0; i < valuesForEqualityCheckA.length; i++) {
if (valuesForEqualityCheckA[i] !== valuesForEqualityCheckB[i]) {
return false;
}
}
return true;
}
export function addWrappedDomEventListener(
storageTarget: EventTarget,
wrappedFunction: EventListenerOrEventListenerObject,
eventName: string,
eventListener: EventListenerOrEventListenerObject,
optionsOrCapture?: EventListenerOptionsOrUseCapture
): EventListenerOrEventListenerObject {
return addWrappedFunction(
storageTarget,
wrappedFunction,
getDomEventListenerValuesForEqualityCheck(eventName, eventListener, optionsOrCapture)
);
}
function getDomEventListenerValuesForEqualityCheck(
eventName: string,
eventListener: EventListenerOrEventListenerObject,
optionsOrCapture?: EventListenerOptionsOrUseCapture
): any[] {
return [eventName, eventListener, getDomEventListenerCaptureValue(optionsOrCapture)];
}
export function getDomEventListenerCaptureValue(optionsOrCapture?: EventListenerOptionsOrUseCapture): boolean {
// > Let capture, passive, and once be the result of flattening more options.
// https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener
//
// > To flatten more options, run these steps:
// > 1. Let capture be the result of flattening options.
// https://dom.spec.whatwg.org/#event-flatten-more
//
// > To flatten options, run these steps:
// > 1. If options is a boolean, then return options.
// > 2. Return options’s capture.
// https://dom.spec.whatwg.org/#concept-flatten-options
//
// > dictionary EventListenerOptions {
// > boolean capture = false;
// > };
// https://dom.spec.whatwg.org/#dom-eventlisteneroptions-capture
if (optionsOrCapture == null) {
return false;
} else if (typeof optionsOrCapture === "object") {
return Boolean(optionsOrCapture.capture);
}
return Boolean(optionsOrCapture);
}
export function popWrappedDomEventListener(
storageTarget: EventTarget,
eventName: string,
eventListener: EventListenerOrEventListenerObject,
optionsOrCapture?: EventListenerOptionsOrUseCapture,
fallback?: EventListenerOrEventListenerObject
): EventListenerOrEventListenerObject | undefined {
return popWrappedFunction(
storageTarget,
getDomEventListenerValuesForEqualityCheck(eventName, eventListener, optionsOrCapture),
fallback
);
}