UNPKG

@launchdarkly/js-sdk-common

Version:
2,500 lines 90.3 kB
/**
 * 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 === 413) {
        return true;
    }
    return isHttpRecoverable(status);
}

/**
 * Returns a promise which errors after t seconds.
 *
 * @param t Timeout in seconds.
 * @param taskName Name of task being timed for logging and error reporting.
 */
function cancelableTimedPromise(t, taskName) {
    let timeout;
    let resolve;
    const promise = new Promise((_res, reject) => {
        resolve = _res;
        timeout = setTimeout(() => {
            const e = `${taskName} timed out after ${t} seconds.`;
            reject(new LDTimeoutError(e));
        }, t * 1000);
    });
    return {
        promise,
        cancel: () => {
            resolve();
            clearTimeout(timeout);
        },
    };
}

function clone(obj) {
    if (obj === undefined || obj === null) {
        return obj;
    }
    return JSON.parse(JSON.stringify(obj));
}

// eslint-disable-next-line import/prefer-default-export
function secondsToMillis(sec) {
    return Math.trunc(sec * 1000);
}

/**
 * Wait before calling the same function. Useful for expensive calls.
 * Adapted from https://amitd.co/code/typescript/debounce.
 *
 * @return The debounced function.
 *
 * @example
 *
 * ```js
 * const debouncedFunction = debounce(e => {
 *   console.log(e);
 * }, 5000);
 *
 * // Console logs 'Hello world again ' after 5 seconds
 * debouncedFunction('Hello world');
 * debouncedFunction('Hello world again');
 * ```
 * @param fn The function to be debounced.
 * @param delayMs Defaults to 5 seconds.
 */
const debounce = (fn, delayMs = 5000) => {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn(...args);
        }, delayMs);
    };
};

const isEmptyObject = (obj) => JSON.stringify(obj) === '{}';

/**
 * Strips all falsy and empty {} from a given object. Returns a new object with only truthy values.
 * Sourced from below but modified to include checks for empty object and ignoring keys.
 * https://www.w3resource.com/javascript-exercises/javascript-array-exercise-47.php
 *
 * @param obj
 * @param ignoreKeys
 */
const deepCompact = (obj, ignoreKeys) => {
    if (!obj) {
        return obj;
    }
    return Object.entries(obj).reduce((acc, [key, value]) => {
        if (Boolean(value) && !isEmptyObject(value) && !ignoreKeys?.includes(key)) {
            acc[key] = typeof value === 'object' ? deepCompact(value, ignoreKeys) : value;
        }
        return acc;
    }, {});
};

/* eslint-disable */
// Ripped from https://github.com/epoberezkin/fast-deep-fastDeepEqual
// {{? it.es6 }}
// var envHasBigInt64Array = typeof BigInt64Array !== 'undefined';
// {{?}}
function fastDeepEqual(a, b) {
    if (a === b)
        return true;
    if (a && b && typeof a == 'object' && typeof b == 'object') {
        if (a.constructor !== b.constructor)
            return false;
        var length, i, keys;
        if (Array.isArray(a)) {
            length = a.length;
            if (length != b.length)
                return false;
            for (i = length; i-- !== 0;)
                if (!fastDeepEqual(a[i], b[i]))
                    return false;
            return true;
        }
        // {{? it.es6 }}
        if (a instanceof Map && b instanceof Map) {
            if (a.size !== b.size)
                return false;
            for (i of a.entries())
                if (!b.has(i[0]))
                    return false;
            for (i of a.entries())
                if (!fastDeepEqual(i[1], b.get(i[0])))
                    return false;
            return true;
        }
        if (a instanceof Set && b instanceof Set) {
            if (a.size !== b.size)
                return false;
            for (i of a.entries())
                if (!b.has(i[0]))
                    return false;
            return true;
        }
        if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
            // @ts-ignore
            length = a.length;
            // @ts-ignore
            if (length != b.length)
                return false;
            for (i = length; i-- !== 0;) {
                // @ts-ignore
                if (a[i] !== b[i])
                    return false;
            }
            return true;
        }
        // {{?}}
        if (a.constructor === RegExp)
            return a.source === b.source && a.flags === b.flags;
        if (a.valueOf !== Object.prototype.valueOf)
            return a.valueOf() === b.valueOf();
        if (a.toString !== Object.prototype.toString)
            return a.toString() === b.toString();
        keys = Object.keys(a);
        length = keys.length;
        if (length !== Object.keys(b).length)
            return false;
        for (i = length; i-- !== 0;)
            if (!Object.prototype.hasOwnProperty.call(b, keys[i]))
                return false;
        for (i = length; i-- !== 0;) {
            var key = keys[i];
            // {{? it.react }}
            // if (key === '_owner' && a.$$typeof) {
            //   // React-specific: avoid traversing React elements' _owner.
            //   //  _owner contains circular references
            //   // and is not needed when comparing the actual elements (and not their owners)
            //   continue;
            // }
            // {{?}}
            if (!fastDeepEqual(a[key], b[key]))
                return false;
        }
        return true;
    }
    // true if both NaN, false otherwise
    return a !== a && b !== b;
}

function defaultHeaders(sdkKey, info, tags, includeAuthorizationHeader = true, userAgentHeaderName = 'user-agent') {
    const { userAgentBase, version, wrapperName, wrapperVersion } = info.sdkData();
    const headers = {
        [userAgentHeaderName]: `${userAgentBase ?? 'NodeJSClient'}/${version}`,
    };
    // edge sdks sets this to false because they use the clientSideID
    // and they don't need the authorization header
    if (includeAuthorizationHeader) {
        headers.authorization = sdkKey;
    }
    if (wrapperName) {
        headers['x-launchdarkly-wrapper'] = wrapperVersion
            ? `${wrapperName}/${wrapperVersion}`
            : wrapperName;
    }
    if (tags?.value) {
        headers['x-launchdarkly-tags'] = tags.value;
    }
    return headers;
}
function httpErrorMessage(err, context, retryMessage) {
    let desc;
    if (err.status) {
        desc = `error ${err.status}${err.status === 401 ? ' (invalid SDK key)' : ''}`;
    }
    else {
        desc = `I/O error (${err.message || 'unknown error'})`;
    }
    const action = retryMessage ?? 'giving up permanently';
    return `Received ${desc} for ${context} - ${action}`;
}
function shouldRetry({ status }) {
    return status ? isHttpRecoverable(status) : true;
}
/**
 * In react-native use base64-js to polyfill btoa. This is safe
 * because the react-native repo uses it too. Set the global.btoa to the encode
 * function of base64-js.
 * https://github.com/beatgammit/base64-js
 * https://github.com/axios/axios/issues/2235#issuecomment-512204616
 *
 * Ripped from https://thewoods.blog/base64url/
 */
const base64UrlEncode = (s, encoding) => encoding.btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

var noop = () => { };

const sleep = async (delayMillis = 1000) => new Promise((resolve) => {
    setTimeout(resolve, delayMillis);
});

/**
 * Returns a promise which errors after t seconds.
 *
 * @param t Timeout in seconds.
 * @param taskName Name of task being timed for logging and error reporting.
 */
const timedPromise = (t, taskName) => new Promise((_res, reject) => {
    setTimeout(() => {
        const e = `${taskName} timed out after ${t} seconds.`;
        reject(new LDTimeoutError(e));
    }, t * 1000);
});

class DiagnosticsManager {
    constructor(sdkKey, _platform, _diagnosticInitConfig) {
        this._platform = _platform;
        this._diagnosticInitConfig = _diagnosticInitConfig;
        this._streamInits = [];
        this._startTime = Date.now();
        this._dataSinceDate = this._startTime;
        this._id = {
            diagnosticId: _platform.crypto.randomUUID(),
            sdkKeySuffix: sdkKey.length > 6 ? sdkKey.substring(sdkKey.length - 6) : sdkKey,
        };
    }
    /**
     * Creates the initial event that is sent by the event processor when the SDK starts up. This will
     * not be repeated during the lifetime of the SDK client.
     */
    createInitEvent() {
        const sdkData = this._platform.info.sdkData();
        const platformData = this._platform.info.platformData();
        return {
            kind: 'diagnostic-init',
            id: this._id,
            creationDate: this._startTime,
            sdk: sdkData,
            configuration: this._diagnosticInitConfig,
            platform: {
                name: platformData.name,
                osArch: platformData.os?.arch,
                osName: platformData.os?.name,
                osVersion: platformData.os?.version,
                ...(platformData.additional || {}),
            },
        };
    }
    /**
     * Records a stream connection attempt (called by the stream processor).
     *
     * @param timestamp Time of the *beginning* of the connection attempt.
     * @param failed True if the connection failed, or we got a read timeout before receiving a "put".
     * @param durationMillis Elapsed time between starting timestamp and when we either gave up/lost
     * the connection or received a successful "put".
     */
    recordStreamInit(timestamp, failed, durationMillis) {
        const item = { timestamp, failed, durationMillis };
        this._streamInits.push(item);
    }
    /**
     * Creates a periodic event containing time-dependent stats, and resets the state of the manager
     * with regard to those stats.
     *
     * Note: the reason droppedEvents, deduplicatedUsers, and eventsInLastBatch are passed into this
     * function, instead of being properties of the DiagnosticsManager, is that the event processor is
     * the one who's calling this function and is also the one who's tracking those stats.
     */
    createStatsEventAndReset(droppedEvents, deduplicatedUsers, eventsInLastBatch) {
        const currentTime = Date.now();
        const evt = {
            kind: 'diagnostic',
            id: this._id,
            creationDate: currentTime,
            dataSinceDate: this._dataSinceDate,
            droppedEvents,
            deduplicatedUsers,
            eventsInLastBatch,
            streamInits: this._streamInits,
        };
        this._streamInits = [];
        this._dataSinceDate = currentTime;
        return evt;
    }
}

/**
 * Different kinds of error which may be encountered during evaluation.
 */
var ErrorKinds;
(function (ErrorKinds) {
    ErrorKinds["MalformedFlag"] = "MALFORMED_FLAG";
    ErrorKinds["UserNotSpecified"] = "USER_NOT_SPECIFIED";
    ErrorKinds["FlagNotFound"] = "FLAG_NOT_FOUND";
    ErrorKinds["ClientNotReady"] = "CLIENT_NOT_READY";
    ErrorKinds["WrongType"] = "WRONG_TYPE";
})(ErrorKinds || (ErrorKinds = {}));
var ErrorKinds$1 = ErrorKinds;

/**
 * Messages for issues which can be encountered processing client requests.
 */
class ClientMessages {
    static invalidMetricValue(badType) {
        return ('The track function was called with a non-numeric "metricValue"' +
            ` (${badType}), only numeric metric values are supported.`);
    }
}
ClientMessages.MissingContextKeyNoEvent = 'Context was unspecified or had no key; event will not be sent';

class EventSender {
    constructor(clientContext, baseHeaders) {
        const { basicConfiguration, platform } = clientContext;
        const { serviceEndpoints: { analyticsEventPath, diagnosticEventPath }, } = basicConfiguration;
        const { crypto, requests } = platform;
        this._defaultHeaders = { ...baseHeaders };
        this._eventsUri = getEventsUri(basicConfiguration.serviceEndpoints, analyticsEventPath, []);
        this._diagnosticEventsUri = getEventsUri(basicConfiguration.serviceEndpoints, diagnosticEventPath, []);
        this._requests = requests;
        this._crypto = crypto;
    }
    async _tryPostingEvents(events, uri, payloadId, canRetry) {
        const tryRes = {
            status: LDDeliveryStatus.Succeeded,
        };
        const headers = {
            ...this._defaultHeaders,
            'content-type': 'application/json',
        };
        if (payloadId) {
            headers['x-launchdarkly-payload-id'] = payloadId;
            headers['x-launchDarkly-event-schema'] = '4';
        }
        let error;
        try {
            const { status, headers: resHeaders } = await this._requests.fetch(uri, {
                headers,
                body: JSON.stringify(events),
                method: 'POST',
                // When sending events from browser environments the request should be completed even
                // if the user is navigating away from the page.
                keepalive: true,
            });
            const serverDate = Date.parse(resHeaders.get('date') || '');
            if (serverDate) {
                tryRes.serverTime = serverDate;
            }
            if (status <= 204) {
                return tryRes;
            }
            error = new LDUnexpectedResponseError(httpErrorMessage({ status, message: 'some events were dropped' }, 'event posting'));
            if (!isHttpRecoverable(status)) {
                // If the HTTP request isn't recoverable. Meaning if we made the same request it
                // would not recover, then we check if a different request could recover.
                // If a different request could not recover, then we shutdown. If a different request could
                // recover, then we just don't retry this specific request.
                if (!isHttpLocallyRecoverable(status)) {
                    tryRes.status = LDDeliveryStatus.FailedAndMustShutDown;
                }
                else {
                    tryRes.status = LDDeliveryStatus.Failed;
                }
                tryRes.error = error;
                return tryRes;
            }
        }
        catch (err) {
            error = err;
        }
        // recoverable but not retrying
        if (error && !canRetry) {
            tryRes.status = LDDeliveryStatus.Failed;
            tryRes.error = error;
            return tryRes;
        }
        // wait 1 second before retrying
        await sleep();
        return this._tryPostingEvents(events, this._eventsUri, payloadId, false);
    }
    async sendEventData(type, data) {
        const payloadId = type === LDEventType.AnalyticsEvents ? this._crypto.randomUUID() : undefined;
        const uri = type === LDEventType.AnalyticsEvents ? this._eventsUri : this._diagnosticEventsUri;
        return this._tryPostingEvents(data, uri, payloadId, true);
    }
}

function isFeature(u) {
    return u.kind === 'feature';
}
function isIdentify(u) {
    return u.kind === 'identify';
}
function isMigration(u) {
    return u.kind === 'migration_op';
}

/**
 * @internal
 */
class SummaryCounter {
    constructor(count, key, value, defValue, version, variation) {
        this.count = count;
        this.key = key;
        this.value = value;
        this.version = version;
        this.variation = variation;
        this.default = defValue;
    }
    increment() {
        this.count += 1;
    }
}

function counterKey(event) {
    return `${event.key}:${event.variation !== null && event.variation !== undefined ? event.variation : ''}:${event.version !== null && event.version !== undefined ? event.version : ''}`;
}
/**
 * @internal
 */
class EventSummarizer {
    constructor() {
        this._startDate = 0;
        this._endDate = 0;
        this._counters = {};
        this._contextKinds = {};
    }
    summarizeEvent(event) {
        if (isFeature(event) && !event.excludeFromSummaries) {
            const countKey = counterKey(event);
            const counter = this._counters[countKey];
            let kinds = this._contextKinds[event.key];
            if (!kinds) {
                kinds = new Set();
                this._contextKinds[event.key] = kinds;
            }
            event.context.kinds.forEach((kind) => kinds.add(kind));
            if (counter) {
                counter.increment();
            }
            else {
                this._counters[countKey] = new SummaryCounter(1, event.key, event.value, event.default, event.version, event.variation);
            }
            if (this._startDate === 0 || event.creationDate < this._startDate) {
                this._startDate = event.creationDate;
            }
            if (event.creationDate > this._endDate) {
                this._endDate = event.creationDate;
            }
        }
    }
    getSummary() {
        const features = Object.values(this._counters).reduce((acc, counter) => {
            let flagSummary = acc[counter.key];
            if (!flagSummary) {
                flagSummary = {
                    default: counter.default,
                    counters: [],
                    contextKinds: [...this._contextKinds[counter.key]],
                };
                acc[counter.key] = flagSummary;
            }
            const counterOut = {
                value: counter.value,
                count: counter.count,
            };
            if (counter.variation !== undefined && counter.variation !== null) {
                counterOut.variation = counter.variation;
            }
            if (counter.version !== undefined && counter.version !== null) {
                counterOut.version = counter.version;
            }
            else {
                counterOut.unknown = true;
            }
            flagSummary.counters.push(counterOut);
            return acc;
        }, {});
        return {
            startDate: this._startDate,
            endDate: this._endDate,
            features,
            kind: 'summary',
        };
    }
    clearSummary() {
        this._startDate = 0;
        this._endDate = 0;
        this._counters = {};
        this._contextKinds = {};
    }
}

class LDInvalidSDKKeyError extends Error {
    constructor(message) {
        super(message);
        this.name = 'LaunchDarklyInvalidSDKKeyError';
    }
}

/**
 * The contents of this file are for event sampling. They are not used for
 * any purpose requiring cryptographic security.
 * */
function shouldSample(ratio) {
    const truncated = Math.trunc(ratio);
    // A radio of 1 means 1 in 1. So that will always sample. No need
    // to draw a random number.
    if (truncated === 1) {
        return true;
    }
    if (truncated === 0) {
        return false;
    }
    // Math.random() * truncated) would return 0, 1, ... (ratio - 1).
    // Checking for any number in the range will have approximately a 1 in X
    // chance. So we check for 0 as it is part of any range.
    return Math.floor(Math.random() * truncated) === 0;
}

class EventProcessor {
    constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true) {
        this._config = _config;
        this._contextDeduplicator = _contextDeduplicator;
        this._diagnosticsManager = _diagnosticsManager;
        this._summarizer = new EventSummarizer();
        this._queue = [];
        this._lastKnownPastTime = 0;
        this._droppedEvents = 0;
        this._deduplicatedUsers = 0;
        this._exceededCapacity = false;
        this._eventsInLastBatch = 0;
        this._shutdown = false;
        this._flushUsersTimer = null;
        this._capacity = _config.eventsCapacity;
        this._logger = clientContext.basicConfiguration.logger;
        this._eventSender = new EventSender(clientContext, baseHeaders);
        this._contextFilter = new ContextFilter(_config.allAttributesPrivate, _config.privateAttributes.map((ref) => new AttributeReference(ref)));
        if (start) {
            this.start();
        }
    }
    start() {
        if (this._contextDeduplicator?.flushInterval !== undefined) {
            this._flushUsersTimer = setInterval(() => {
                this._contextDeduplicator?.flush();
            }, this._contextDeduplicator.flushInterval * 1000);
        }
        this._flushTimer = setInterval(async () => {
            try {
                await this.flush();
            }
            catch (e) {
                // Log errors and swallow them
                this._logger?.debug(`Flush failed: ${e}`);
            }
        }, this._config.flushInterval * 1000);
        if (this._diagnosticsManager) {
            const initEvent = this._diagnosticsManager.createInitEvent();
            this._postDiagnosticEvent(initEvent);
            this._diagnosticsTimer = setInterval(() => {
                const statsEvent = this._diagnosticsManager.createStatsEventAndReset(this._droppedEvents, this._deduplicatedUsers, this._eventsInLastBatch);
                this._droppedEvents = 0;
                this._deduplicatedUsers = 0;
                this._postDiagnosticEvent(statsEvent);
            }, this._config.diagnosticRecordingInterval * 1000);
        }
        this._logger?.debug('Started EventProcessor.');
    }
    _postDiagnosticEvent(event) {
        this._eventSender.sendEventData(LDEventType.DiagnosticEvent, event);
    }
    close() {
        clearInterval(this._flushTimer);
        if (this._flushUsersTimer) {
            clearInterval(this._flushUsersTimer);
        }
        if (this._diagnosticsTimer) {
            clearInterval(this._diagnosticsTimer);
        }
    }
    async flush() {
        if (this._shutdown) {
            throw new LDInvalidSDKKeyError('Events cannot be posted because a permanent error has been encountered. ' +
                'This is most likely an invalid SDK key. The specific error information ' +
                'is logged independently.');
        }
        const eventsToFlush = this._queue;
        this._queue = [];
        const summary = this._summarizer.getSummary();
        this._summarizer.clearSummary();
        if (Object.keys(summary.features).length) {
            eventsToFlush.push(summary);
        }
        if (!eventsToFlush.length) {
            return;
        }
        this._eventsInLastBatch = eventsToFlush.length;
        this._logger?.debug('Flushing %d events', eventsToFlush.length);
        await this._tryPostingEvents(eventsToFlush);
    }
    sendEvent(inputEvent) {
        if (this._shutdown) {
            return;
        }
        if (isMigration(inputEvent)) {
            // These conditions are not combined, because we always want to stop
            // processing at this point for a migration event. It cannot generate
            // an index event or debug event.
            if (shouldSample(inputEvent.samplingRatio)) {
                const migrationEvent = {
                    ...inputEvent,
                    context: inputEvent.context ? this._contextFilter.filter(inputEvent.context) : undefined,
                };
                if (migrationEvent.samplingRatio === 1) {
                    delete migrationEvent.samplingRatio;
                }
                this._enqueue(migrationEvent);
            }
            return;
        }
        this._summarizer.summarizeEvent(inputEvent);
        const isFeatureEvent = isFeature(inputEvent);
        const addFullEvent = (isFeatureEvent && inputEvent.trackEvents) || !isFeatureEvent;
        const addDebugEvent = this._shouldDebugEvent(inputEvent);
        const isIdentifyEvent = isIdentify(inputEvent);
        const shouldNotDeduplicate = this._contextDeduplicator?.processContext(inputEvent.context);
        // If there is no cache, then it will never be in the cache.
        if (!shouldNotDeduplicate) {
            if (!isIdentifyEvent) {
                this._deduplicatedUsers += 1;
            }
        }
        const addIndexEvent = shouldNotDeduplicate && !isIdentifyEvent;
        if (addIndexEvent) {
            this._enqueue(this._makeOutputEvent({
                kind: 'index',
                creationDate: inputEvent.creationDate,
                context: inputEvent.context,
                samplingRatio: 1,
            }, false));
        }
        if (addFullEvent && shouldSample(inputEvent.samplingRatio)) {
            this._enqueue(this._makeOutputEvent(inputEvent, false));
        }
        if (addDebugEvent && shouldSample(inputEvent.samplingRatio)) {
            this._enqueue(this._makeOutputEvent(inputEvent, true));
        }
    }
    _makeOutputEvent(event, debug) {
        switch (event.kind) {
            case 'feature': {
                const out = {
                    kind: debug ? 'debug' : 'feature',
                    creationDate: event.creationDate,
                    context: this._contextFilter.filter(event.context, !debug),
                    key: event.key,
                    value: event.value,
                    default: event.default,
                };
                if (event.samplingRatio !== 1) {
                    out.samplingRatio = event.samplingRatio;
                }
                if (event.prereqOf) {
                    out.prereqOf = event.prereqOf;
                }
                if (event.variation !== undefined) {
                    out.variation = event.variation;
                }
                if (event.version !== undefined) {
                    out.version = event.version;
                }
                if (event.reason) {
                    out.reason = event.reason;
                }
                return out;
            }
            case 'index': // Intentional fallthrough.
            case 'identify': {
                const out = {
                    kind: event.kind,
                    creationDate: event.creationDate,
                    context: this._contextFilter.filter(event.context),
                };
                if (event.samplingRatio !== 1) {
                    out.samplingRatio = event.samplingRatio;
                }
                return out;
            }
            case 'custom': {
                const out = {
                    kind: 'custom',
                    creationDate: event.creationDate,
                    key: event.key,
                    context: this._contextFilter.filter(event.context),
                };
                if (event.samplingRatio !== 1) {
                    out.samplingRatio = event.samplingRatio;
                }
                if (event.data !== undefined) {
                    out.data = event.data;
                }
                if (event.metricValue !== undefined) {
                    out.metricValue = event.metricValue;
                }
                if (event.url !== undefined) {
                    out.url = event.url;
                }
                return out;
            }
            case 'click': {
                const out = {
                    kind: 'click',
                    creationDate: event.creationDate,
                    contextKeys: event.context.kindsAndKeys,
                    key: event.key,
                    url: event.url,
                    selector: event.selector,
                };
                return out;
            }
            case 'pageview': {
                const out = {
                    kind: 'pageview',
                    creationDate: event.creationDate,
                    contextKeys: event.context.kindsAndKeys,
                    key: event.key,
                    url: event.url,
                };
                return out;
            }
            default:
                // This would happen during the addition of a new event type to the SDK.
                return event;
        }
    }
    _enqueue(event) {
        if (this._queue.length < this._capacity) {
            this._queue.push(event);
            this._exceededCapacity = false;
        }
        else {
            if (!this._exceededCapacity) {
                this._exceededCapacity = true;
                this._logger?.warn('Exceeded event queue capacity. Increase capacity to avoid dropping events.');
            }
            this._droppedEvents += 1;
        }
    }
    _shouldDebugEvent(event) {
        return (isFeature(event) &&
            event.debugEventsUntilDate &&
            event.debugEventsUntilDate > this._lastKnownPastTime &&
            event.debugEventsUntilDate > Date.now());
    }
    async _tryPostingEvents(events) {
        const res = await this._eventSender.sendEventData(LDEventType.AnalyticsEvents, events);
        if (res.status === LDDeliveryStatus.FailedAndMustShutDown) {
            this._shutdown = true;
        }
        if (res.serverTime) {
            this._lastKnownPastTime = res.serverTime;
        }
        if (res.error) {
            throw res.error;
        }
    }
}

class InputCustomEvent {
    constructor(context, key, data, metricValue, 
    // Currently custom events are not sampled, but this is here to make the handling
    // code more uniform.
    samplingRatio = 1, 
    // Browser SDKs can include a URL for custom events.
    url) {
        this.context = context;
        this.key = key;
        this.data = data;
        this.metricValue = metricValue;
        this.samplingRatio = samplingRatio;
        this.url = url;
        this.kind = 'custom';
        this.creationDate = Date.now();
        this.context = context;
    }
}

class InputEvalEvent {
    constructor(withReasons, context, key, value, defValue, // default is a reserved keyword in this context.
    version, variation, trackEvents, prereqOf, reason, debugEventsUntilDate, excludeFromSummaries, samplingRatio = 1) {
        this.withReasons = withReasons;
        this.context = context;
        this.key = key;
        this.samplingRatio = samplingRatio;
        this.kind = 'feature';
        this.creationDate = Date.now();
        this.value = value;
        this.default = defValue;
        if (version !== undefined) {
            this.version = version;
        }
        if (variation !== undefined) {
            this.variation = variation;
        }
        if (trackEvents !== undefined) {
            this.trackEvents = trackEvents;
        }
        if (prereqOf !== undefined) {
            this.prereqOf = prereqOf;
        }
        if (reason !== undefined) {
            this.reason = reason;
        }
        if (debugEventsUntilDate !== undefined) {
            this.debugEventsUntilDate = debugEventsUntilDate;
        }
        if (excludeFromSummaries !== undefined) {
            this.excludeFromSummaries = excludeFromSummaries;
        }
    }
}

class InputIdentifyEvent {
    constructor(context, samplingRatio = 1) {
        this.context = context;
        this.samplingRatio = samplingRatio;
        this.kind = 'identify';
        this.creationDate = Date.now();
    }
}

class NullEventProcessor {
    close() { }
    async flush() {
        // empty comment to keep ts and eslint happy
    }
    sendEvent() { }
}

class EventFactoryBase {
    constructor(_withReasons) {
        this._withReasons = _withReasons;
    }
    evalEvent(e) {
        return new InputEvalEvent(this._withReasons, e.context, e.flagKey, e.value, e.defaultVal, e.version, 
        // Exclude null as a possibility.
        e.variation ?? undefined, e.trackEvents || e.addExperimentData, e.prereqOfFlagKey, this._withReasons || e.addExperimentData ? e.reason : undefined, e.debugEventsUntilDate, e.excludeFromSummaries, e.samplingRatio);
    }
    unknownFlagEvent(key, defVal, context) {
        return new InputEvalEvent(this._withReasons, context, key, defVal, defVal, 
        // This isn't ideal, but the purpose of the factory is to at least
        // handle this situation.
        undefined, // version
        undefined, // variation index
        undefined, // track events
        undefined, // prereqOf
        undefined, // reason
        undefined, // debugEventsUntilDate
        undefined, // exclude from summaries
        undefined);
    }
    /* eslint-disable-next-line class-methods-use-this */
    identifyEvent(context) {
        // Currently sampling for identify events is always 1.
        return new InputIdentifyEvent(context, 1);
    }
    /* eslint-disable-next-line class-methods-use-this */
    customEvent(key, context, data, metricValue, samplingRatio = 1) {
        return new InputCustomEvent(context, key, data ?? undefined, metricValue ?? undefined, samplingRatio);
    }
}

/**
 * A FDv2 PayloadReader can be used to parse payloads from a stream of FDv2 events. It will send payloads
 * to the PayloadListeners as the payloads are received. Invalid series of events may be dropped silently,
 * but the payload reader will continue to operate.
 */
class PayloadReader {
    /**
     * Creates a PayloadReader
     *
     * @param eventStream event stream of FDv2 events
     * @param _objProcessors defines object processors for each object kind.
     * @param _errorHandler that will be called with errors as they are encountered
     * @param _logger for logging
     */
    constructor(eventStream, _objProcessors, _errorHandler, _logger) {
        this._objProcessors = _objProcessors;
        this._errorHandler = _errorHandler;
        this._logger = _logger;
        this._listeners = [];
        this._tempId = undefined;
        this._tempBasis = undefined;
        this._tempUpdates = [];
        this._processServerIntent = (data) => {
            // clear state in prep for handling data
            this._resetState();
            // if there's no payloads, return
            if (!data.payloads.length) {
                return;
            }
            // at the time of writing this, it was agreed upon that SDKs could assume exactly 1 element in this list.  In the future, a negotiation of protocol version will be required to remove this assumption.
            const payload = data.payloads[0];
            switch (payload?.code) {
                case 'xfer-full':
                    this._tempBasis = true;
                    break;
                case 'xfer-changes':
                case 'none':
                    this._tempBasis = false;
                    break;
                default:
                    // unrecognized intent code, return
                    return;
            }
            this._tempId = payload?.id;
        };
        this._processPutObject = (data) => {
            // if the following properties haven't been provided by now, we should ignore the event
            if (!this._tempId || // server intent hasn't been recieved yet.
                !data.kind ||
                !data.key ||
                !data.version ||
                !data.object) {
                return;
            }
            const obj = this._processObj(data.kind, data.object);
            if (!obj) {
                this._logger?.warn(`Unable to prcoess object for kind: '${data.kind}'`);
                // ignore unrecognized kinds
                return;
            }
            this._tempUpdates.push({
                kind: data.kind,
                key: data.key,
                version: data.version,
                object: obj,
                // intentionally omit deleted for this put
            });
        };
        this._processDeleteObject = (data) => {
            // if the following properties haven't been provided by now, we should ignore the event
            if (!this._tempId || !data.kind || !data.key || !data.version) {
                return;
            }
            this._tempUpdates.push({
                kind: data.kind,
                key: data.key,
                version: data.version,
                // intentionally omit object for this delete
                deleted: true,
            });
        };
        this._processPayloadTransferred = (data) => {
            // if the following properties haven't been provided by now, we should reset
            if (!this._tempId || // server intent hasn't been recieved yet.
                !data.state ||
                !data.version ||
                this._tempBasis === undefined) {
                this._resetState(); // a reset is best defensive action since payload transferred terminates a payload
                return;
            }
            const payload = {
                id: this._tempId,
                version: data.version,
                state: data.state,
                basis: this._tempBasis,
                updates: this._tempUpdates,
            };
            this._listeners.forEach((it) => it(payload));
            this._resetState();
        };
        this._processGoodbye = (data) => {
            this._logger?.info(`Goodbye was received from the LaunchDarkly connection with reason: ${data.reason}.`);
            this._resetState();
        };
        this._processError = (data) => {
            this._logger?.info(`An issue was encountered receiving updates for payload ${this._tempId} with reason: ${data.reason}. Automatic retry will occur.`);
            this._resetState();
        };
        this._attachHandler(eventStream, 'server-intent', this._processServerIntent);
        this._attachHandler(eventStream, 'put-object', this._processPutObject);
        this._attachHandler(eventStream, 'delete-object', this._processDeleteObject);
        this._attachHandler(eventStream, 'payload-transferred', this._processPayloadTransferred);
        this._attachHandler(eventStream, 'goodbye', this._processGoodbye);
        this._attachHandler(eventStream, 'error', this._processError);
    }
    addPayloadListener(listener) {
        this._listeners.push(listener);
    }
    removePayloadListener(listener) {
        const index = this._listeners.indexOf(listener, 0);
        if (index > -1) {
            this._listeners.splice(index, 1);
        }
    }
    _attachHandler(stream, eventName, processor) {
        stream.addEventListener(eventName, async (event) => {
            if (event?.data) {
                this._logger?.debug(`Received ${eventName} event.  Data is ${event.data}`);
                try {
                    processor(JSON.parse(event.data));
                }
                catch {
                    this._logger?.error(`Stream received data that was unable to be processed in "${eventName}" message`);
                    this._logger?.debug(`Data follows: ${event.data}`);
                    this._errorHandler?.(DataSourceErrorKind.InvalidData, 'Malformed data in event stream');
                }
            }
            else {
                this._errorHandler?.(DataSourceErrorKind.Unknown, 'Unexpected message from event stream');
            }
        });
    }
    _processObj(kind, jsonObj) {
        return this._objProcessors[kind]?.(jsonObj);
    }
    _resetState() {
        this._tempId = undefined;
        this._tempBasis = undefined;
        this._tempUpdates = [];
    }
}

var index = /*#__PURE__*/Object.freeze({
    __proto__: null,
    ClientMessages: ClientMessages,
    DiagnosticsManager: DiagnosticsManager,
    ErrorKinds: ErrorKinds$1,
    EventFactoryBase: EventFactoryBase,
    EventProcessor: EventProcessor,
    InputCustomEvent: InputCustomEvent,
    InputEvalEvent: InputEvalEvent,
    InputIdentifyEvent: InputIdentifyEvent,
    NullEventProcessor: NullEventProcessor,
    PayloadReader: PayloadReader,
    isLegacyUser: isLegacyUser,
    isMultiKind: isMultiKind,
    isSingleKind: isSingleKind,
    shouldSample: shouldSample
});

export { ApplicationTags, AttributeReference, AutoEnvAttributes, BasicLogger, ClientContext, Context, ContextFilter, DataSourceErrorKind, DateValidator, FactoryOrInstance, Function, KindValidator, LDClientError, LDFileDataSourceError, LDPollingError, LDStreamingError, LDTimeoutError, LDUnexpectedResponseError, NullableBoolean, NumberWithMinimum, OptionMessages, SafeLogger, ServiceEndpoints, StringMatchingRegex, Type, TypeArray, TypeValidators, base64UrlEncode, cancelableTimedPromise, clone, createSafeLogger, debounce, deepCompact, defaultHeaders, fastDeepEqual, getEventsUri, getPollingUri, getStreamingUri, httpErrorMessage, index as internal, isHttpLocallyRecoverable, isHttpRecoverable, noop, secondsToMillis, shouldRetry, sleep, index$1 as subsystem, timedPromise };
//# sourceMappingURL=index.mjs.map