UNPKG

salsify-experiences-sdk

Version:

SDK to be used by commerce websites to implement product experiences.

164 lines 6.44 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PerProductConfigCache = exports.selectSource = exports.isPerProductConfig = void 0; const request_1 = __importStar(require("../utils/request")); const hash_1 = require("../utils/hash"); function isPerProductConfig(config, logger, url) { const errProps = { errorType: 'validation', errorContext: 'per-product config', }; if (typeof config !== 'object' || config === null) { logger?.log('error', { ...errProps, errorMessage: `${url} does not contain an object` }); return false; } if (!('content' in config)) { logger?.log('error', { ...errProps, errorMessage: `${url} does not contain a 'content' key` }); return false; } if (!Array.isArray(config.content)) { logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} is not an array` }); return false; } if (config.content.length === 0) { logger?.log('error', { ...errProps, errorMessage: `'content' array in ${url} has length 0` }); return false; } if (!config.content.every(source => isSource(source, logger, url))) { return false; } if (Math.abs(1 - config.content.reduce((sumOfWeights, source) => (sumOfWeights += source.weight), 0)) > 0.001) { logger?.log('error', { ...errProps, errorMessage: `sum of source weights in 'content' in ${url} does not equal 1` }); return false; } return true; } exports.isPerProductConfig = isPerProductConfig; function isSource(thing, logger, url) { const errProps = { errorType: 'validation', errorContext: 'per-product config', }; if (typeof thing !== 'object' || thing === null) { logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} contains a source that is not an object` }); return false; } if (!('source' in thing)) { logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} contains a source that is missing a 'source' key`, }); return false; } if (typeof thing.source !== 'string' && thing.source !== null) { logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} contains a source with an invalid 'source' value`, }); return false; } if (!('weight' in thing)) { logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} contains a source that is missing a 'weight' key`, }); return false; } if (typeof thing.weight !== 'number') { logger?.log('error', { ...errProps, errorMessage: `'content' in ${url} contains a source with an invalid 'weight' value`, }); return false; } return true; } function selectSource(args) { const contentString = JSON.stringify(args.content); const hashString = `${args.productId}${contentString}${args.sessionId}`; const hash = (0, hash_1.murmurHash3)(hashString, 0); // divde by max 32 bit integer to scale to 0..1 const scaledHash = hash / 0xffffffff; // use scaled hash as a "random" number in weighted random selection let sum = 0; const cumulativeWeights = args.content.map(source => (sum += source.weight)); const index = cumulativeWeights.findIndex(cWeight => cWeight >= scaledHash); return args.content[index]; } exports.selectSource = selectSource; class PerProductConfigCache { #cache = new Map(); #logger; /** @internal */ constructor(logger) { this.#logger = logger; } async getConfig(cdnPath) { return this.#cache.has(cdnPath) ? this.#cache.get(cdnPath) : this.#fetchConfig(cdnPath); } async #fetchConfig(cdnPath) { const configUrl = `${cdnPath}/config.json`; let response; try { response = await request_1.default.get(configUrl); } catch (error) { const errorMessage = error instanceof Error ? error.message : undefined; this.#logger.log('error', { errorContext: 'per-product config', errorType: 'fetch', errorMessage: `Error fetching ${configUrl}: ${errorMessage}`, }); return undefined; } if (response.headers.get(request_1.Header.ContentLength) === '0') { return this.#cacheAndReturn(cdnPath, undefined); } let data; try { data = await response.json(); } catch (error) { const errorMessage = error instanceof Error ? error.message : undefined; this.#logger.log('error', { errorContext: 'per-product config', errorType: 'parse', errorMessage: `Error parsing ${configUrl}: ${errorMessage}`, }); return this.#cacheAndReturn(cdnPath, undefined); } if (isPerProductConfig(data, this.#logger, configUrl)) { return this.#cacheAndReturn(cdnPath, data); } return this.#cacheAndReturn(cdnPath, undefined); } #cacheAndReturn(cdnPath, config) { this.#cache.set(cdnPath, config); return config; } } exports.PerProductConfigCache = PerProductConfigCache; //# sourceMappingURL=perProductConfig.js.map