UNPKG

@dash0/sdk-web

Version:

Dash0's Web SDK to collect telemetry from end-users' web browsers

158 lines (140 loc) 5.35 kB
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 ); }