@launchdarkly/js-sdk-common
Version:
LaunchDarkly SDK for JavaScript - common code
1,424 lines (1,406 loc) • 90.3 kB
JavaScript
/**
* Converts a literal to a ref string.
* @param value
* @returns An escaped literal which can be used as a ref.
*/
function toRefString(value) {
return `/${value.replace(/~/g, '~0').replace(/\//g, '~1')}`;
}
/**
* Produce a literal from a ref component.
* @param ref
* @returns A literal version of the ref.
*/
function unescape(ref) {
return ref.indexOf('~') ? ref.replace(/~1/g, '/').replace(/~0/g, '~') : ref;
}
function getComponents(reference) {
const referenceWithoutPrefix = reference.startsWith('/') ? reference.substring(1) : reference;
return referenceWithoutPrefix.split('/').map((component) => unescape(component));
}
function isLiteral(reference) {
return !reference.startsWith('/');
}
function validate(reference) {
return !reference.match(/\/\/|(^\/.*~[^0|^1])|~$/);
}
class AttributeReference {
/**
* Take an attribute reference string, or literal string, and produce
* an attribute reference.
*
* Legacy user objects would have been created with names not
* references. So, in that case, we need to use them as a component
* without escaping them.
*
* e.g. A user could contain a custom attribute of `/a` which would
* become the literal `a` if treated as a reference. Which would cause
* it to no longer be redacted.
* @param refOrLiteral The attribute reference string or literal string.
* @param literal it true the value should be treated as a literal.
*/
constructor(refOrLiteral, literal = false) {
if (!literal) {
this.redactionName = refOrLiteral;
if (refOrLiteral === '' || refOrLiteral === '/' || !validate(refOrLiteral)) {
this.isValid = false;
this._components = [];
return;
}
if (isLiteral(refOrLiteral)) {
this._components = [refOrLiteral];
}
else if (refOrLiteral.indexOf('/', 1) < 0) {
this._components = [unescape(refOrLiteral.slice(1))];
}
else {
this._components = getComponents(refOrLiteral);
}
// The items inside of '_meta' are not intended to be addressable.
// Excluding it as a valid reference means that we can make it non-addressable
// without having to copy all the attributes out of the context object
// provided by the user.
if (this._components[0] === '_meta') {
this.isValid = false;
}
else {
this.isValid = true;
}
}
else {
const literalVal = refOrLiteral;
this._components = [literalVal];
this.isValid = literalVal !== '';
// Literals which start with '/' need escaped to prevent ambiguity.
this.redactionName = literalVal.startsWith('/') ? toRefString(literalVal) : literalVal;
}
}
get(target) {
const { _components: components, isValid } = this;
if (!isValid) {
return undefined;
}
let current = target;
// This doesn't use a range based for loops, because those use generators.
// See `no-restricted-syntax`.
// It also doesn't use a collection method because this logic is more
// straightforward with a loop.
for (let index = 0; index < components.length; index += 1) {
const component = components[index];
if (current !== null &&
current !== undefined &&
// See https://eslint.org/docs/rules/no-prototype-builtins
Object.prototype.hasOwnProperty.call(current, component) &&
typeof current === 'object' &&
// We do not want to allow indexing into an array.
!Array.isArray(current)) {
current = current[component];
}
else {
return undefined;
}
}
return current;
}
getComponent(depth) {
return this._components[depth];
}
get depth() {
return this._components.length;
}
get isKind() {
return this._components.length === 1 && this._components[0] === 'kind';
}
compare(other) {
return (this.depth === other.depth &&
this._components.every((value, index) => value === other.getComponent(index)));
}
}
/**
* For use as invalid references when deserializing Flag/Segment data.
*/
AttributeReference.InvalidReference = new AttributeReference('');
/* eslint-disable class-methods-use-this */
/* eslint-disable max-classes-per-file */
/**
* Validate a factory or instance.
*/
class FactoryOrInstance {
is(factoryOrInstance) {
if (Array.isArray(factoryOrInstance)) {
return false;
}
const anyFactory = factoryOrInstance;
const typeOfFactory = typeof anyFactory;
return typeOfFactory === 'function' || typeOfFactory === 'object';
}
getType() {
return 'factory method or object';
}
}
/**
* Validate a basic type.
*/
class Type {
constructor(typeName, example) {
this._typeName = typeName;
this.typeOf = typeof example;
}
is(u) {
if (Array.isArray(u)) {
return false;
}
return typeof u === this.typeOf;
}
getType() {
return this._typeName;
}
}
/**
* Validate an array of the specified type.
*
* This does not validate instances of types. All class instances
* of classes will simply objects.
*/
class TypeArray {
constructor(typeName, example) {
this._typeName = typeName;
this.typeOf = typeof example;
}
is(u) {
if (Array.isArray(u)) {
if (u.length > 0) {
return u.every((val) => typeof val === this.typeOf);
}
return true;
}
return false;
}
getType() {
return this._typeName;
}
}
/**
* Validate a value is a number and is greater or eval than a minimum.
*/
class NumberWithMinimum extends Type {
constructor(min) {
super(`number with minimum value of ${min}`, 0);
this.min = min;
}
is(u) {
return typeof u === this.typeOf && u >= this.min;
}
}
/**
* Validate a value is a string and it matches the given expression.
*/
class StringMatchingRegex extends Type {
constructor(expression) {
super(`string matching ${expression}`, '');
this.expression = expression;
}
is(u) {
return typeof u === 'string' && !!u.match(this.expression);
}
}
/**
* Validate a value is a function.
*/
class Function {
is(u) {
// We cannot inspect the parameters and there isn't really
// a generic function type we can instantiate.
// So the type guard is here just to make TS comfortable
// calling something after using this guard.
return typeof u === 'function';
}
getType() {
return 'function';
}
}
class NullableBoolean {
is(u) {
return typeof u === 'boolean' || typeof u === 'undefined' || u === null;
}
getType() {
return 'boolean | undefined | null';
}
}
// Our reference SDK, Go, parses date/time strings with the time.RFC3339Nano format.
// This regex should match strings that are valid in that format, and no others.
// Acceptable:
// 2019-10-31T23:59:59Z, 2019-10-31T23:59:59.100Z,
// 2019-10-31T23:59:59-07, 2019-10-31T23:59:59-07:00, etc.
// Unacceptable: no "T", no time zone designation
const DATE_REGEX = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d*)?(Z|[-+]\d\d(:\d\d)?)/;
/**
* Validate a value is a date. Values which are numbers are treated as dates and any string
* which if compliant with `time.RFC3339Nano` is a date.
*/
class DateValidator {
is(u) {
return typeof u === 'number' || (typeof u === 'string' && DATE_REGEX.test(u));
}
getType() {
return 'date';
}
}
/**
* Validates that a string is a valid kind.
*/
class KindValidator extends StringMatchingRegex {
constructor() {
super(/^(\w|\.|-)+$/);
}
is(u) {
return super.is(u) && u !== 'kind';
}
}
/**
* A set of standard type validators.
*/
class TypeValidators {
static createTypeArray(typeName, example) {
return new TypeArray(typeName, example);
}
static numberWithMin(min) {
return new NumberWithMinimum(min);
}
static stringMatchingRegex(expression) {
return new StringMatchingRegex(expression);
}
}
TypeValidators.String = new Type('string', '');
TypeValidators.Number = new Type('number', 0);
TypeValidators.ObjectOrFactory = new FactoryOrInstance();
TypeValidators.Object = new Type('object', {});
TypeValidators.StringArray = new TypeArray('string[]', '');
TypeValidators.Boolean = new Type('boolean', true);
TypeValidators.Function = new Function();
TypeValidators.Date = new DateValidator();
TypeValidators.Kind = new KindValidator();
TypeValidators.NullableBoolean = new NullableBoolean();
/**
* Check if a context is a single kind context.
* @param context
* @returns true if the context is a single kind context.
*/
function isSingleKind(context) {
if ('kind' in context) {
return TypeValidators.String.is(context.kind) && context.kind !== 'multi';
}
return false;
}
/**
* Check if a context is a multi-kind context.
* @param context
* @returns true if it is a multi-kind context.
*/
function isMultiKind(context) {
if ('kind' in context) {
return TypeValidators.String.is(context.kind) && context.kind === 'multi';
}
return false;
}
/**
* Check if a context is a legacy user context.
* @param context
* @returns true if it is a legacy user context.
*/
function isLegacyUser(context) {
return !('kind' in context) || context.kind === null || context.kind === undefined;
}
// The general strategy for the context is to transform the passed in context
// as little as possible. We do convert the legacy users to a single kind
// context, but we do not translate all passed contexts into a rigid structure.
// The context will have to be copied for events, but we want to avoid any
// copying that we can.
// So we validate that the information we are given is correct, and then we
// just proxy calls with a nicely typed interface.
// This is to reduce work on the hot-path. Later, for event processing, deeper
// cloning of the context will be done.
// When no kind is specified, then this kind will be used.
const DEFAULT_KIND = 'user';
// The API allows for calling with an `LDContext` which is
// `LDUser | LDSingleKindContext | LDMultiKindContext`. When ingesting a context
// first the type must be determined to allow us to put it into a consistent type.
/**
* The partial URL encoding is needed because : is a valid character in context keys.
*
* Partial encoding is the replacement of all colon (:) characters with the URL
* encoded equivalent (%3A) and all percent (%) characters with the URL encoded
* equivalent (%25).
* @param key The key to encode.
* @returns Partially URL encoded key.
*/
function encodeKey(key) {
if (key.includes('%') || key.includes(':')) {
return key.replace(/%/g, '%25').replace(/:/g, '%3A');
}
return key;
}
/**
* Check if the given value is a LDContextCommon.
* @param kindOrContext
* @returns true if it is an LDContextCommon
*
* Due to a limitation in the expressiveness of these highly polymorphic types any field
* in a multi-kind context can either be a context or 'kind'. So we need to re-assure
* the compiler that it isn't the word multi.
*
* Because we do not allow top level values in a multi-kind context we can validate
* that as well.
*/
function isContextCommon(kindOrContext) {
return kindOrContext && TypeValidators.Object.is(kindOrContext);
}
/**
* Validate a context kind.
* @param kind
* @returns true if the kind is valid.
*/
function validKind(kind) {
return TypeValidators.Kind.is(kind);
}
/**
* Validate a context key.
* @param key
* @returns true if the key is valid.
*/
function validKey(key) {
return TypeValidators.String.is(key) && key !== '';
}
function processPrivateAttributes(privateAttributes, literals = false) {
if (privateAttributes) {
return privateAttributes.map((privateAttribute) => new AttributeReference(privateAttribute, literals));
}
return [];
}
function defined(value) {
return value !== null && value !== undefined;
}
/**
* Convert a legacy user to a single kind context.
* @param user
* @returns A single kind context.
*/
function legacyToSingleKind(user) {
const singleKindContext = {
// Key was coerced to a string for eval and events, so we can do that up-front.
...(user.custom || []),
kind: 'user',
key: String(user.key),
};
// For legacy users we never established a difference between null
// and undefined for inputs. Because anonymous can be used in evaluations
// we would want it to not possibly match true/false unless defined.
// Which is different than coercing a null/undefined anonymous as `false`.
if (defined(user.anonymous)) {
const anonymous = !!user.anonymous;
delete singleKindContext.anonymous;
singleKindContext.anonymous = anonymous;
}
if (user.name !== null && user.name !== undefined) {
singleKindContext.name = user.name;
}
if (user.ip !== null && user.ip !== undefined) {
singleKindContext.ip = user.ip;
}
if (user.firstName !== null && user.firstName !== undefined) {
singleKindContext.firstName = user.firstName;
}
if (user.lastName !== null && user.lastName !== undefined) {
singleKindContext.lastName = user.lastName;
}
if (user.email !== null && user.email !== undefined) {
singleKindContext.email = user.email;
}
if (user.avatar !== null && user.avatar !== undefined) {
singleKindContext.avatar = user.avatar;
}
if (user.country !== null && user.country !== undefined) {
singleKindContext.country = user.country;
}
if (user.privateAttributeNames !== null && user.privateAttributeNames !== undefined) {
singleKindContext._meta = {
privateAttributes: user.privateAttributeNames,
};
}
// We are not pulling private attributes over because we will serialize
// those from attribute references for events.
return singleKindContext;
}
/**
* Container for a context/contexts. Because contexts come from external code
* they must be thoroughly validated and then formed to comply with
* the type system.
*/
class Context {
/**
* Contexts should be created using the static factory method {@link Context.fromLDContext}.
* @param kind The kind of the context.
*
* The factory methods are static functions within the class because they access private
* implementation details, so they cannot be free functions.
*/
constructor(valid, kind, message) {
this._isMulti = false;
this._isUser = false;
this._wasLegacy = false;
this._contexts = {};
this.kind = kind;
this.valid = valid;
this.message = message;
}
static _contextForError(kind, message) {
return new Context(false, kind, message);
}
static _getValueFromContext(reference, context) {
if (!context || !reference.isValid) {
return undefined;
}
if (reference.depth === 1 && reference.getComponent(0) === 'anonymous') {
return !!context?.anonymous;
}
return reference.get(context);
}
_contextForKind(kind) {
if (this._isMulti) {
return this._contexts[kind];
}
if (this.kind === kind) {
return this._context;
}
return undefined;
}
static _fromMultiKindContext(context) {
const kinds = Object.keys(context).filter((key) => key !== 'kind');
const kindsValid = kinds.every(validKind);
if (!kinds.length) {
return Context._contextForError('multi', 'A multi-kind context must contain at least one kind');
}
if (!kindsValid) {
return Context._contextForError('multi', 'Context contains invalid kinds');
}
const privateAttributes = {};
let contextsAreObjects = true;
const contexts = kinds.reduce((acc, kind) => {
const singleContext = context[kind];
if (isContextCommon(singleContext)) {
acc[kind] = singleContext;
privateAttributes[kind] = processPrivateAttributes(singleContext._meta?.privateAttributes);
}
else {
// No early break isn't the most efficient, but it is an error condition.
contextsAreObjects = false;
}
return acc;
}, {});
if (!contextsAreObjects) {
return Context._contextForError('multi', 'Context contained contexts that were not objects');
}
if (!Object.values(contexts).every((part) => validKey(part.key))) {
return Context._contextForError('multi', 'Context contained invalid keys');
}
// There was only a single kind in the multi-kind context.
// So we can just translate this to a single-kind context.
if (kinds.length === 1) {
const kind = kinds[0];
const created = new Context(true, kind);
created._context = { ...contexts[kind], kind };
created._privateAttributeReferences = privateAttributes;
created._isUser = kind === 'user';
return created;
}
const created = new Context(true, context.kind);
created._contexts = contexts;
created._privateAttributeReferences = privateAttributes;
created._isMulti = true;
return created;
}
static _fromSingleKindContext(context) {
const { key, kind } = context;
const kindValid = validKind(kind);
const keyValid = validKey(key);
if (!kindValid) {
return Context._contextForError(kind ?? 'unknown', 'The kind was not valid for the context');
}
if (!keyValid) {
return Context._contextForError(kind, 'The key for the context was not valid');
}
// The JSON interfaces uses dangling _.
// eslint-disable-next-line no-underscore-dangle
const privateAttributeReferences = processPrivateAttributes(context._meta?.privateAttributes);
const created = new Context(true, kind);
created._isUser = kind === 'user';
created._context = context;
created._privateAttributeReferences = {
[kind]: privateAttributeReferences,
};
return created;
}
static _fromLegacyUser(context) {
const keyValid = context.key !== undefined && context.key !== null;
// For legacy users we allow empty keys.
if (!keyValid) {
return Context._contextForError('user', 'The key for the context was not valid');
}
const created = new Context(true, 'user');
created._isUser = true;
created._wasLegacy = true;
created._context = legacyToSingleKind(context);
created._privateAttributeReferences = {
user: processPrivateAttributes(context.privateAttributeNames, true),
};
return created;
}
/**
* Attempt to create a {@link Context} from an {@link LDContext}.
* @param context The input context to create a Context from.
* @returns a {@link Context}, if the context was not valid, then the returned contexts `valid`
* property will be false.
*/
static fromLDContext(context) {
if (!context) {
return Context._contextForError('unknown', 'No context specified. Returning default value');
}
if (isSingleKind(context)) {
return Context._fromSingleKindContext(context);
}
if (isMultiKind(context)) {
return Context._fromMultiKindContext(context);
}
if (isLegacyUser(context)) {
return Context._fromLegacyUser(context);
}
return Context._contextForError('unknown', 'Context was not of a valid kind');
}
/**
* Creates a {@link LDContext} from a {@link Context}.
* @param context to be converted
* @returns an {@link LDContext} if input was valid, otherwise undefined
*/
static toLDContext(context) {
if (!context.valid) {
return undefined;
}
const contexts = context.getContexts();
if (!context._isMulti) {
return contexts[0][1];
}
const result = {
kind: 'multi',
};
contexts.forEach((kindAndContext) => {
const kind = kindAndContext[0];
const nestedContext = kindAndContext[1];
result[kind] = nestedContext;
});
return result;
}
/**
* Attempt to get a value for the given context kind using the given reference.
* @param reference The reference to the value to get.
* @param kind The kind of the context to get the value for.
* @returns a value or `undefined` if one is not found.
*/
valueForKind(reference, kind = DEFAULT_KIND) {
if (reference.isKind) {
return this.kinds;
}
return Context._getValueFromContext(reference, this._contextForKind(kind));
}
/**
* Attempt to get a key for the specified kind.
* @param kind The kind to get a key for.
* @returns The key for the specified kind, or undefined.
*/
key(kind = DEFAULT_KIND) {
return this._contextForKind(kind)?.key;
}
/**
* True if this is a multi-kind context.
*/
get isMultiKind() {
return this._isMulti;
}
/**
* Get the canonical key for this context.
*/
get canonicalKey() {
if (this._isUser) {
return this._context.key;
}
if (this._isMulti) {
return Object.keys(this._contexts)
.sort()
.map((key) => `${key}:${encodeKey(this._contexts[key].key)}`)
.join(':');
}
return `${this.kind}:${encodeKey(this._context.key)}`;
}
/**
* Get the kinds of this context.
*/
get kinds() {
if (this._isMulti) {
return Object.keys(this._contexts);
}
return [this.kind];
}
/**
* Get the kinds, and their keys, for this context.
*/
get kindsAndKeys() {
if (this._isMulti) {
return Object.entries(this._contexts).reduce((acc, [kind, context]) => {
acc[kind] = context.key;
return acc;
}, {});
}
return { [this.kind]: this._context.key };
}
/**
* Get the attribute references.
*
* @param kind
*/
privateAttributes(kind) {
return this._privateAttributeReferences?.[kind] || [];
}
/**
* Get the underlying context objects from this context.
*
* This method is intended to be used in event generation.
*
* The returned objects should not be modified.
*/
getContexts() {
if (this._isMulti) {
return Object.entries(this._contexts);
}
return [[this.kind, this._context]];
}
get legacy() {
return this._wasLegacy;
}
}
Context.UserKind = DEFAULT_KIND;
// _meta is part of the specification.
// These attributes cannot be removed via a private attribute.
const protectedAttributes = ['key', 'kind', '_meta', 'anonymous'].map((str) => new AttributeReference(str, true));
// Attributes that should be stringified for legacy users.
const legacyTopLevelCopyAttributes = [
'name',
'ip',
'firstName',
'lastName',
'email',
'avatar',
'country',
];
function compare(a, b) {
return a.depth === b.length && b.every((value, index) => value === a.getComponent(index));
}
function cloneWithRedactions(target, references) {
const stack = [];
const cloned = {};
const excluded = [];
stack.push(...Object.keys(target).map((key) => ({
key,
ptr: [key],
source: target,
parent: cloned,
visited: [target],
})));
while (stack.length) {
const item = stack.pop();
const redactRef = references.find((ref) => compare(ref, item.ptr));
if (!redactRef) {
const value = item.source[item.key];
// Handle null because it overlaps with object, which we will want to handle later.
if (value === null) {
item.parent[item.key] = value;
}
else if (Array.isArray(value)) {
item.parent[item.key] = [...value];
}
else if (typeof value === 'object') {
// Arrays and null must already be handled.
// Prevent cycles by not visiting the same object
// with in the same branch. Different branches
// may contain the same object.
//
// Same object visited twice in different branches.
// A -> B -> D
// -> C -> D
// This is fine, which is why it doesn't just check if the object
// was visited ever.
if (!item.visited.includes(value)) {
item.parent[item.key] = {};
stack.push(...Object.keys(value).map((key) => ({
key,
ptr: [...item.ptr, key],
source: value,
parent: item.parent[item.key],
visited: [...item.visited, value],
})));
}
}
else {
item.parent[item.key] = value;
}
}
else {
excluded.push(redactRef.redactionName);
}
}
return { cloned, excluded: excluded.sort() };
}
class ContextFilter {
constructor(_allAttributesPrivate, _privateAttributes) {
this._allAttributesPrivate = _allAttributesPrivate;
this._privateAttributes = _privateAttributes;
}
filter(context, redactAnonymousAttributes = false) {
const contexts = context.getContexts();
if (contexts.length === 1) {
return this._filterSingleKind(context, contexts[0][1], contexts[0][0], redactAnonymousAttributes);
}
const filteredMulti = {
kind: 'multi',
};
contexts.forEach(([kind, single]) => {
filteredMulti[kind] = this._filterSingleKind(context, single, kind, redactAnonymousAttributes);
});
return filteredMulti;
}
_getAttributesToFilter(context, single, kind, redactAllAttributes) {
return (redactAllAttributes
? Object.keys(single).map((k) => new AttributeReference(k, true))
: [...this._privateAttributes, ...context.privateAttributes(kind)]).filter((attr) => !protectedAttributes.some((protectedAttr) => protectedAttr.compare(attr)));
}
_filterSingleKind(context, single, kind, redactAnonymousAttributes) {
const redactAllAttributes = this._allAttributesPrivate || (redactAnonymousAttributes && single.anonymous === true);
const { cloned, excluded } = cloneWithRedactions(single, this._getAttributesToFilter(context, single, kind, redactAllAttributes));
if (context.legacy) {
legacyTopLevelCopyAttributes.forEach((name) => {
if (name in cloned) {
cloned[name] = String(cloned[name]);
}
});
}
if (excluded.length) {
if (!cloned._meta) {
cloned._meta = {};
}
cloned._meta.redactedAttributes = excluded;
}
if (cloned._meta) {
delete cloned._meta.privateAttributes;
if (Object.keys(cloned._meta).length === 0) {
delete cloned._meta;
}
}
return cloned;
}
}
var DataSourceErrorKind;
(function (DataSourceErrorKind) {
/// An unexpected error, such as an uncaught exception, further
/// described by the error message.
DataSourceErrorKind["Unknown"] = "UNKNOWN";
/// An I/O error such as a dropped connection.
DataSourceErrorKind["NetworkError"] = "NETWORK_ERROR";
/// The LaunchDarkly service returned an HTTP response with an error
/// status, available in the status code.
DataSourceErrorKind["ErrorResponse"] = "ERROR_RESPONSE";
/// The SDK received malformed data from the LaunchDarkly service.
DataSourceErrorKind["InvalidData"] = "INVALID_DATA";
})(DataSourceErrorKind || (DataSourceErrorKind = {}));
class LDFileDataSourceError extends Error {
constructor(message) {
super(message);
this.name = 'LaunchDarklyFileDataSourceError';
}
}
class LDPollingError extends Error {
constructor(kind, message, status, recoverable = true) {
super(message);
this.kind = kind;
this.status = status;
this.name = 'LaunchDarklyPollingError';
this.recoverable = recoverable;
}
}
class LDStreamingError extends Error {
constructor(kind, message, code, recoverable = true) {
super(message);
this.kind = kind;
this.code = code;
this.name = 'LaunchDarklyStreamingError';
this.recoverable = recoverable;
}
}
/* eslint-disable import/prefer-default-export */
/**
* Enable / disable Auto environment attributes. When enabled, the SDK will automatically
* provide data about the mobile environment where the application is running. This data makes it simpler to target
* your mobile customers based on application name or version, or on device characteristics including manufacturer,
* model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. To learn more,
* read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes).
* for more documentation.
*/
var AutoEnvAttributes;
(function (AutoEnvAttributes) {
AutoEnvAttributes[AutoEnvAttributes["Disabled"] = 0] = "Disabled";
AutoEnvAttributes[AutoEnvAttributes["Enabled"] = 1] = "Enabled";
})(AutoEnvAttributes || (AutoEnvAttributes = {}));
var LDEventType;
(function (LDEventType) {
LDEventType[LDEventType["AnalyticsEvents"] = 0] = "AnalyticsEvents";
LDEventType[LDEventType["DiagnosticEvent"] = 1] = "DiagnosticEvent";
})(LDEventType || (LDEventType = {}));
var LDDeliveryStatus;
(function (LDDeliveryStatus) {
LDDeliveryStatus[LDDeliveryStatus["Succeeded"] = 0] = "Succeeded";
LDDeliveryStatus[LDDeliveryStatus["Failed"] = 1] = "Failed";
LDDeliveryStatus[LDDeliveryStatus["FailedAndMustShutDown"] = 2] = "FailedAndMustShutDown";
})(LDDeliveryStatus || (LDDeliveryStatus = {}));
var index$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
get LDDeliveryStatus () { return LDDeliveryStatus; },
get LDEventType () { return LDEventType; }
});
/**
* Attempt to produce a string representation of a value.
* The format should be roughly comparable to `util.format`
* aside from object which will be JSON versus the `util.inspect`
* format.
* @param val
* @returns A string representation of the value if possible.
*/
function tryStringify(val) {
if (typeof val === 'string') {
return val;
}
if (val === undefined) {
return 'undefined';
}
if (val === null) {
return 'null';
}
if (Object.prototype.hasOwnProperty.call(val, 'toString')) {
try {
return val.toString();
}
catch {
/* Keep going */
}
}
if (typeof val === 'bigint') {
return `${val}n`;
}
try {
return JSON.stringify(val);
}
catch (error) {
if (error instanceof TypeError && error.message.indexOf('circular') >= 0) {
return '[Circular]';
}
return '[Not Stringifiable]';
}
}
/**
* Attempt to produce a numeric representation.
* BigInts have an `n` suffix.
* @param val
* @returns The numeric representation or 'NaN' if not numeric.
*/
function toNumber(val) {
// Symbol has to be treated special because it will
// throw an exception if an attempt is made to convert it.
if (typeof val === 'symbol') {
return 'NaN';
}
if (typeof val === 'bigint') {
return `${val}n`;
}
return String(Number(val));
}
/**
* Attempt to produce an integer representation.
* BigInts have an `n` suffix.
* @param val
* @returns The integer representation or 'NaN' if not numeric.
*/
function toInt(val) {
if (typeof val === 'symbol') {
return 'NaN';
}
if (typeof val === 'bigint') {
return `${val}n`;
}
return String(parseInt(val, 10));
}
/**
* Attempt to produce a float representation.
* BigInts have an `n` suffix.
* @param val
* @returns The integer representation or 'NaN' if not numeric.
*/
function toFloat(val) {
if (typeof val === 'symbol') {
return 'NaN';
}
return String(parseFloat(val));
}
// Based on:
// https://nodejs.org/api/util.html#utilformatformat-args
// The result will not match node exactly, but it should get the
// right information through.
const escapes = {
s: (val) => tryStringify(val),
d: (val) => toNumber(val),
i: (val) => toInt(val),
f: (val) => toFloat(val),
j: (val) => tryStringify(val),
o: (val) => tryStringify(val),
// eslint-disable-next-line @typescript-eslint/naming-convention
O: (val) => tryStringify(val),
c: () => '',
};
/**
* A basic formatted for use where `util.format` is not available.
* This will not be as performant, but it will produce formatted
* messages.
*
* @internal
*
* @param args
* @returns Formatted string.
*/
function format(...args) {
const formatString = args.shift();
if (TypeValidators.String.is(formatString)) {
let out = '';
let i = 0;
while (i < formatString.length) {
const char = formatString.charAt(i);
if (char === '%') {
const nextIndex = i + 1;
if (nextIndex < formatString.length) {
const nextChar = formatString.charAt(i + 1);
if (nextChar in escapes && args.length) {
const value = args.shift();
// This rule is for math.
// eslint-disable-next-line no-unsafe-optional-chaining
out += escapes[nextChar]?.(value);
}
else if (nextChar === '%') {
out += '%';
}
else {
out += `%${nextChar}`;
}
i += 2;
}
}
else {
out += char;
i += 1;
}
}
// If there are any args left after we exhaust the format string
// then just stick those on the end.
if (args.length) {
if (out.length) {
out += ' ';
}
out += args.map(tryStringify).join(' ');
}
return out;
}
return args.map(tryStringify).join(' ');
}
var LogPriority;
(function (LogPriority) {
LogPriority[LogPriority["debug"] = 0] = "debug";
LogPriority[LogPriority["info"] = 1] = "info";
LogPriority[LogPriority["warn"] = 2] = "warn";
LogPriority[LogPriority["error"] = 3] = "error";
LogPriority[LogPriority["none"] = 4] = "none";
})(LogPriority || (LogPriority = {}));
const LEVEL_NAMES = ['debug', 'info', 'warn', 'error', 'none'];
/**
* A basic logger which handles filtering by level.
*
* With the default options it will write to `console.error`
* and it will use the formatting provided by `console.error`.
* If the destination is overwritten, then it will use an included
* formatter similar to `util.format`.
*
* If a formatter is available, then that should be overridden
* as well for performance.
*/
class BasicLogger {
/**
* This should only be used as a default fallback and not as a convenient
* solution. In most cases you should construct a new instance with the
* appropriate options for your specific needs.
*/
static get() {
return new BasicLogger({});
}
constructor(options) {
this._logLevel = LogPriority[options.level ?? 'info'] ?? LogPriority.info;
this._name = options.name ?? 'LaunchDarkly';
this._formatter = options.formatter;
if (typeof options.destination === 'object') {
this._destinations = {
[LogPriority.debug]: options.destination.debug,
[LogPriority.info]: options.destination.info,
[LogPriority.warn]: options.destination.warn,
[LogPriority.error]: options.destination.error,
};
}
else if (typeof options.destination === 'function') {
const { destination } = options;
this._destinations = {
[LogPriority.debug]: destination,
[LogPriority.info]: destination,
[LogPriority.warn]: destination,
[LogPriority.error]: destination,
};
}
}
_tryFormat(...args) {
try {
if (this._formatter) {
// In case the provided formatter fails.
return this._formatter?.(...args);
}
return format(...args);
}
catch {
return format(...args);
}
}
_tryWrite(destination, msg) {
try {
destination(msg);
}
catch {
// eslint-disable-next-line no-console
console.error(msg);
}
}
_log(level, args) {
if (level >= this._logLevel) {
const prefix = `${LEVEL_NAMES[level]}: [${this._name}]`;
try {
const destination = this._destinations?.[level];
if (destination) {
this._tryWrite(destination, `${prefix} ${this._tryFormat(...args)}`);
}
else {
// `console.error` has its own formatter.
// So we don't need to do anything.
// eslint-disable-next-line no-console
console.error(...args);
}
}
catch {
// If all else fails do not break.
// eslint-disable-next-line no-console
console.error(...args);
}
}
}
error(...args) {
this._log(LogPriority.error, args);
}
warn(...args) {
this._log(LogPriority.warn, args);
}
info(...args) {
this._log(LogPriority.info, args);
}
debug(...args) {
this._log(LogPriority.debug, args);
}
}
const loggerRequirements = {
error: TypeValidators.Function,
warn: TypeValidators.Function,
info: TypeValidators.Function,
debug: TypeValidators.Function,
};
/**
* The safeLogger logic exists because we allow the application to pass in a custom logger, but
* there is no guarantee that the logger works correctly and if it ever throws exceptions there
* could be serious consequences (e.g. an uncaught exception within an error event handler, due
* to the SDK trying to log the error, can terminate the application). An exception could result
* from faulty logic in the logger implementation, or it could be that this is not a logger at
* all but some other kind of object; the former is handled by a catch block that logs an error
* message to the SDK's default logger, and we can at least partly guard against the latter by
* checking for the presence of required methods at configuration time.
*/
class SafeLogger {
/**
* Construct a safe logger with the specified logger.
* @param logger The logger to use.
* @param fallback A fallback logger to use in case an issue is encountered using
* the provided logger.
*/
constructor(logger, fallback) {
Object.entries(loggerRequirements).forEach(([level, validator]) => {
if (!validator.is(logger[level])) {
throw new Error(`Provided logger instance must support logger.${level}(...) method`);
// Note that the SDK normally does not throw exceptions to the application, but that rule
// does not apply to LDClient.init() which will throw an exception if the parameters are so
// invalid that we cannot proceed with creating the client. An invalid logger meets those
// criteria since the SDK calls the logger during nearly all of its operations.
}
});
this._logger = logger;
this._fallback = fallback;
}
_log(level, args) {
try {
this._logger[level](...args);
}
catch {
// If all else fails do not break.
this._fallback[level](...args);
}
}
error(...args) {
this._log('error', args);
}
warn(...args) {
this._log('warn', args);
}
info(...args) {
this._log('info', args);
}
debug(...args) {
this._log('debug', args);
}
}
const createSafeLogger = (logger) => {
const basicLogger = new BasicLogger({
level: 'info',
// eslint-disable-next-line no-console
destination: console.error,
formatter: format,
});
return logger ? new SafeLogger(logger, basicLogger) : basicLogger;
};
/**
* Messages for issues which can be encountered from processing the configuration options.
*/
class OptionMessages {
static deprecated(oldName, newName) {
return `"${oldName}" is deprecated, please use "${newName}"`;
}
static optionBelowMinimum(name, value, min) {
return `Config option "${name}" had invalid value of ${value}, using minimum of ${min} instead`;
}
static unknownOption(name) {
return `Ignoring unknown config option "${name}"`;
}
static wrongOptionType(name, expectedType, actualType) {
return `Config option "${name}" should be of type ${expectedType}, got ${actualType}, using default value`;
}
static wrongOptionTypeBoolean(name, actualType) {
return `Config option "${name}" should be a boolean, got ${actualType}, converting to boolean`;
}
static invalidTagValue(name) {
return `Config option "${name}" must only contain letters, numbers, ., _ or -.`;
}
static tagValueTooLong(name) {
return `Value of "${name}" was longer than 64 characters and was discarded.`;
}
static partialEndpoint(name) {
return `You have set custom uris without specifying the ${name} URI; connections may not work properly`;
}
}
/**
* Expression to validate characters that are allowed in tag keys and values.
*/
const allowedTagCharacters = /^(\w|\.|-)+$/;
const regexValidator = TypeValidators.stringMatchingRegex(allowedTagCharacters);
const tagValidator = {
is: (u, name) => {
if (regexValidator.is(u)) {
if (u.length > 64) {
return { valid: false, message: OptionMessages.tagValueTooLong(name) };
}
return { valid: true };
}
return { valid: false, message: OptionMessages.invalidTagValue(name) };
},
};
/**
* Class for managing tags.
*/
class ApplicationTags {
constructor(options) {
const tags = {};
const application = options?.application;
const logger = options?.logger;
if (application) {
Object.entries(application).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
const { valid, message } = tagValidator.is(value, `application.${key}`);
if (!valid) {
logger?.warn(message);
}
else if (key === 'versionName') {
tags[`application-version-name`] = [value];
}
else {
tags[`application-${key}`] = [value];
}
}
});
}
const tagKeys = Object.keys(tags);
if (tagKeys.length) {
this.value = tagKeys
.sort()
.flatMap((key) => tags[key].sort().map((value) => `${key}/${value}`))
.join(' ');
}
}
}
/**
* The client context provides basic configuration and platform support which are required
* when building SDK components.
*/
class ClientContext {
constructor(sdkKey, configuration, platform) {
this.platform = platform;
this.basicConfiguration = {
tags: configuration.tags,
logger: configuration.logger,
offline: configuration.offline,
serviceEndpoints: configuration.serviceEndpoints,
sdkKey,
};
}
}
function canonicalizeUri(uri) {
return uri.replace(/\/+$/, '');
}
function canonicalizePath(path) {
return path.replace(/^\/+/, '').replace(/\?$/, '');
}
/**
* Specifies the base service URIs used by SDK components.
*/
class ServiceEndpoints {
constructor(streaming, polling, events = ServiceEndpoints.DEFAULT_EVENTS, analyticsEventPath = '/bulk', diagnosticEventPath = '/diagnostic', includeAuthorizationHeader = true, payloadFilterKey) {
this.streaming = canonicalizeUri(streaming);
this.polling = canonicalizeUri(polling);
this.events = canonicalizeUri(events);
this.analyticsEventPath = analyticsEventPath;
this.diagnosticEventPath = diagnosticEventPath;
this.includeAuthorizationHeader = includeAuthorizationHeader;
this.payloadFilterKey = payloadFilterKey;
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention
ServiceEndpoints.DEFAULT_EVENTS = 'https://events.launchdarkly.com';
function getWithParams(uri, parameters) {
if (parameters.length === 0) {
return uri;
}
const parts = parameters.map(({ key, value }) => `${key}=${value}`);
return `${uri}?${parts.join('&')}`;
}
/**
* Get the URI for the streaming endpoint.
*
* @param endpoints The service endpoints.
* @param path The path to the resource, devoid of any query parameters or hrefs.
* @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you.
*/
function getStreamingUri(endpoints, path, parameters) {
const canonicalizedPath = canonicalizePath(path);
const combinedParameters = [...parameters];
if (endpoints.payloadFilterKey) {
combinedParameters.push({ key: 'filter', value: endpoints.payloadFilterKey });
}
return getWithParams(`${endpoints.streaming}/${canonicalizedPath}`, combinedParameters);
}
/**
* Get the URI for the polling endpoint.
*
* @param endpoints The service endpoints.
* @param path The path to the resource, devoid of any query parameters or hrefs.
* @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you.
*/
function getPollingUri(endpoints, path, parameters) {
const canonicalizedPath = canonicalizePath(path);
const combinedParameters = [...parameters];
if (endpoints.payloadFilterKey) {
combinedParameters.push({ key: 'filter', value: endpoints.payloadFilterKey });
}
return getWithParams(`${endpoints.polling}/${canonicalizedPath}`, combinedParameters);
}
/**
* Get the URI for the events endpoint.
*
* @param endpoints The service endpoints.
* @param path The path to the resource, devoid of any query parameters or hrefs.
* @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you.
*/
function getEventsUri(endpoints, path, parameters) {
const canonicalizedPath = canonicalizePath(path);
return getWithParams(`${endpoints.events}/${canonicalizedPath}`, parameters);
}
// These classes are of trivial complexity. If they become
// more complex, then they could be independent files.
/* eslint-disable max-classes-per-file */
class LDUnexpectedResponseError extends Error {
constructor(message) {
super(message);
this.name = 'LaunchDarklyUnexpectedResponseError';
}
}
class LDClientError extends Error {
constructor(message) {
super(message);
this.name = 'LaunchDarklyClientError';
}
}
class LDTimeoutError extends Error {
constructor(message) {
super(message);
this.name = 'LaunchDarklyTimeoutError';
}
}
/**
* Check if the HTTP error is recoverable. This will return false if a request
* made with any payload could not recover. If the reason for the failure
* is payload specific, for instance a payload that is too large, then
* it could recover with a different payload.
*/
function isHttpRecoverable(status) {
if (status >= 400 && status < 500) {
return status === 400 || status === 408 || status === 429;
}
return true;
}
/**
* Returns true if the status could recover for a different payload.
*
* When used with event processing this indicates that we should discard
* the payload, but that a subsequent payload may succeed. Therefore we should
* not stop event processing.
*/
function isHttpLocallyRecoverable(status) {
if (status