UNPKG

@thumbmarkjs/thumbmarkjs

Version:

![GitHub package.json dynamic](https://img.shields.io/github/package-json/version/ilkkapeltola/thumbmarkjs) ![NPM Version](https://img.shields.io/npm/v/@thumbmarkjs/thumbmarkjs) ![NPM Downloads](https://img.shields.io/npm/dm/%40thumbmarkjs%2Fthumbmarkjs

177 lines (161 loc) 5.74 kB
import { optionsInterface, DEFAULT_API_ENDPOINT, OptionsAfterDefaults} from '../options'; import { componentInterface } from '../factory'; import { getVisitorId, setVisitorId } from '../utils/visitorId'; import { getVersion } from "../utils/version"; import { hash } from '../utils/hash'; import { stableStringify } from '../utils/stableStringify'; import { getCache, getApiResponseExpiry, setCache } from "../utils/cache"; // ===================== Types & Interfaces ===================== /** * Info returned from the API (IP, classification, uniqueness, etc) */ export interface infoInterface { ip_address?: { ip_address: string, ip_identifier: string, autonomous_system_number: number, ip_version: 'v6' | 'v4', }, classification?: { tor: boolean, vpn: boolean, bot: boolean, datacenter: boolean, danger_level: number, // 5 is highest and should be blocked. 0 is no danger. }, uniqueness?: { score: number | string }, timed_out?: boolean; // added for timeout handling } /** * API response structure */ export interface apiResponse { info?: infoInterface; version?: string; components?: componentInterface; visitorId?: string; thumbmark?: string; } // ===================== API Call Logic ===================== let currentApiPromise: Promise<apiResponse> | null = null; let apiPromiseResult: apiResponse | null = null; /** * Calls the Thumbmark API with the given components, using caching and deduplication. * Returns a promise for the API response or null on error. */ export const getApiPromise = ( options: OptionsAfterDefaults, components: componentInterface ): Promise<apiResponse | null> => { // 1. If a result is already cached and caching is enabled, return it. if (options.cache_api_call) { // Check the in-memory cache if(apiPromiseResult) { return Promise.resolve(apiPromiseResult); } // Check the localStorage cache const cached = getCachedApiResponse(options); if(cached) { return Promise.resolve(cached); } } // 2. If a request is already in flight, return that promise to prevent duplicate calls. if (currentApiPromise) { return currentApiPromise; } // 3. Otherwise, initiate a new API call with timeout. const apiEndpoint = options.api_endpoint || DEFAULT_API_ENDPOINT; const endpoint = `${apiEndpoint}/thumbmark`; const visitorId = getVisitorId(options); const requestBody: any = { components, options, clientHash: hash(stableStringify(components)), version: getVersion() }; if (visitorId) { requestBody.visitorId = visitorId; } const fetchPromise = fetch(endpoint, { method: 'POST', headers: { 'x-api-key': options.api_key!, 'Authorization': 'custom-authorized', 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }) .then(response => { // Handle HTTP errors that aren't network errors if (!response.ok) { if (response.status === 403) { throw new Error('INVALID_API_KEY'); } throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { // Handle visitor ID from server response if (data.visitorId && data.visitorId !== visitorId) { setVisitorId(data.visitorId, options); } apiPromiseResult = data; // Cache the successful result setCachedApiResponse(options, data); // Cache to localStorage according to options currentApiPromise = null; // Clear the in-flight promise return data; }) .catch(error => { console.error('Error fetching pro data', error); currentApiPromise = null; // Also clear the in-flight promise on error // For 403 errors, propagate the error instead of returning null if (error.message === 'INVALID_API_KEY') { throw error; } // Return null instead of a string to prevent downstream crashes return null; }); // Timeout logic const timeoutMs = options.timeout || 5000; const timeoutPromise = new Promise<apiResponse>((resolve) => { setTimeout(() => { resolve({ info: { timed_out: true }, version: getVersion(), }); }, timeoutMs); }); currentApiPromise = Promise.race([fetchPromise, timeoutPromise]); return currentApiPromise; }; /** * If a valid cached api response exists, returns it * @param options */ export function getCachedApiResponse( options: Pick<OptionsAfterDefaults, 'property_name_factory'>, ): apiResponse | undefined { const cache = getCache(options); if (cache && cache.apiResponse && cache.apiResponseExpiry && Date.now() <= cache.apiResponseExpiry) { return cache.apiResponse; } return; } /** * Writes the api response to the cache according to the options * @param options * @param response */ export function setCachedApiResponse( options: Pick<OptionsAfterDefaults, 'cache_api_call' | 'cache_lifetime_in_ms' | 'property_name_factory'>, response: apiResponse ): void { if(!options.cache_api_call || !options.cache_lifetime_in_ms) { return; } setCache(options, { apiResponseExpiry: getApiResponseExpiry(options), apiResponse: response, }); }