salsify-experiences-sdk
Version:
SDK to be used by commerce websites to implement product experiences.
164 lines • 6.44 kB
JavaScript
;
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