@launchdarkly/js-server-sdk-common
Version:
LaunchDarkly Server SDK for JavaScript - common code
323 lines • 12.5 kB
JavaScript
// The deserialization will be updating parameter values, so we don't need this
// warning in this file.
Object.defineProperty(exports, "__esModule", { value: true });
exports.deserializeSegment = exports.serializeSegment = exports.deserializeFlag = exports.serializeFlag = exports.deserializeDelete = exports.deserializePatch = exports.deserializePoll = exports.deserializeAll = exports.processSegment = exports.processFlag = exports.replacer = exports.nullReplacer = void 0;
/* eslint-disable no-param-reassign */
const js_sdk_common_1 = require("@launchdarkly/js-sdk-common");
const VersionedDataKinds_1 = require("./VersionedDataKinds");
// The max size where we use an array instead of a set.
const TARGET_LIST_ARRAY_CUTOFF = 100;
/**
* Performs deep removal of null values.
*
* Does not remove null values from arrays.
*
* Note: This is a non-recursive implementation for performance and to avoid
* potential stack overflows.
*
* @param target The target to remove null values from.
* @param excludeKeys A list of top-level keys to exclude from null removal.
*/
function nullReplacer(target, excludeKeys) {
const stack = [];
if (target === null || target === undefined) {
return;
}
const filteredEntries = Object.entries(target).filter(([key, _value]) => !(excludeKeys === null || excludeKeys === void 0 ? void 0 : excludeKeys.includes(key)));
stack.push(...filteredEntries.map(([key, value]) => ({
key,
value,
parent: target,
})));
while (stack.length) {
const item = stack.pop();
// Do not remove items from arrays.
if (item.value === null && !Array.isArray(item.parent)) {
delete item.parent[item.key];
}
else if (typeof item.value === 'object' && item.value !== null) {
// Add all the children to the stack. This includes array children.
// The items in the array could themselves be objects which need nulls
// removed from them.
stack.push(...Object.entries(item.value).map(([key, value]) => ({
key,
value,
parent: item.value,
})));
}
}
}
exports.nullReplacer = nullReplacer;
/**
* For use when serializing flags/segments. This will ensure local types
* are converted to the appropriate JSON representation.
* @param this The scope containing the key/value.
* @param key The key of the item being visited.
* @param value The value of the item being visited.
* @returns A transformed value for serialization.
*
* @internal
*/
function replacer(key, value) {
if (value instanceof js_sdk_common_1.AttributeReference) {
return undefined;
}
if (Array.isArray(value)) {
if (value[0] && value[0] instanceof js_sdk_common_1.AttributeReference) {
return undefined;
}
}
// Allow null/undefined values to pass through without modification.
if (value === null || value === undefined) {
return value;
}
if (value.generated_includedSet) {
value.included = [...value.generated_includedSet];
delete value.generated_includedSet;
}
if (value.generated_excludedSet) {
value.excluded = [...value.generated_excludedSet];
delete value.generated_excludedSet;
}
if (value.includedContexts) {
value.includedContexts.forEach((target) => {
if (target.generated_valuesSet) {
target.values = [...target.generated_valuesSet];
}
delete target.generated_valuesSet;
});
}
if (value.excludedContexts) {
value.excludedContexts.forEach((target) => {
if (target.generated_valuesSet) {
target.values = [...target.generated_valuesSet];
}
delete target.generated_valuesSet;
});
}
return value;
}
exports.replacer = replacer;
function processRollout(rollout) {
if (rollout && rollout.bucketBy) {
rollout.bucketByAttributeReference = new js_sdk_common_1.AttributeReference(rollout.bucketBy, !rollout.contextKind);
}
}
/**
* @internal
*/
function processFlag(flag) {
var _a;
nullReplacer(flag, ['variations']);
if (flag.fallthrough && flag.fallthrough.rollout) {
const rollout = flag.fallthrough.rollout;
processRollout(rollout);
}
(_a = flag === null || flag === void 0 ? void 0 : flag.rules) === null || _a === void 0 ? void 0 : _a.forEach((rule) => {
var _a;
processRollout(rule.rollout);
(_a = rule === null || rule === void 0 ? void 0 : rule.clauses) === null || _a === void 0 ? void 0 : _a.forEach((clause) => {
if (clause && clause.attribute) {
// Clauses before U2C would have had literals for attributes.
// So use the contextKind to indicate if this is new or old data.
clause.attributeReference = new js_sdk_common_1.AttributeReference(clause.attribute, !clause.contextKind);
}
else if (clause) {
clause.attributeReference = js_sdk_common_1.AttributeReference.InvalidReference;
}
});
});
}
exports.processFlag = processFlag;
/**
* @internal
*/
function processSegment(segment) {
var _a, _b, _c, _d, _e;
nullReplacer(segment);
if (((_a = segment === null || segment === void 0 ? void 0 : segment.included) === null || _a === void 0 ? void 0 : _a.length) && segment.included.length > TARGET_LIST_ARRAY_CUTOFF) {
segment.generated_includedSet = new Set(segment.included);
delete segment.included;
}
if (((_b = segment === null || segment === void 0 ? void 0 : segment.excluded) === null || _b === void 0 ? void 0 : _b.length) && segment.excluded.length > TARGET_LIST_ARRAY_CUTOFF) {
segment.generated_excludedSet = new Set(segment.excluded);
delete segment.excluded;
}
if ((_c = segment === null || segment === void 0 ? void 0 : segment.includedContexts) === null || _c === void 0 ? void 0 : _c.length) {
segment.includedContexts.forEach((target) => {
var _a;
if (((_a = target === null || target === void 0 ? void 0 : target.values) === null || _a === void 0 ? void 0 : _a.length) && target.values.length > TARGET_LIST_ARRAY_CUTOFF) {
target.generated_valuesSet = new Set(target.values);
// Currently typing is non-optional, so we don't delete it.
target.values = [];
}
});
}
if ((_d = segment === null || segment === void 0 ? void 0 : segment.excludedContexts) === null || _d === void 0 ? void 0 : _d.length) {
segment.excludedContexts.forEach((target) => {
var _a;
if (((_a = target === null || target === void 0 ? void 0 : target.values) === null || _a === void 0 ? void 0 : _a.length) && target.values.length > TARGET_LIST_ARRAY_CUTOFF) {
target.generated_valuesSet = new Set(target.values);
// Currently typing is non-optional, so we don't delete it.
target.values = [];
}
});
}
(_e = segment === null || segment === void 0 ? void 0 : segment.rules) === null || _e === void 0 ? void 0 : _e.forEach((rule) => {
var _a;
if (rule.bucketBy) {
// Rules before U2C would have had literals for attributes.
// So use the rolloutContextKind to indicate if this is new or old data.
rule.bucketByAttributeReference = new js_sdk_common_1.AttributeReference(rule.bucketBy, !rule.rolloutContextKind);
}
(_a = rule === null || rule === void 0 ? void 0 : rule.clauses) === null || _a === void 0 ? void 0 : _a.forEach((clause) => {
if (clause && clause.attribute) {
// Clauses before U2C would have had literals for attributes.
// So use the contextKind to indicate if this is new or old data.
clause.attributeReference = new js_sdk_common_1.AttributeReference(clause.attribute, !clause.contextKind);
}
else if (clause) {
clause.attributeReference = js_sdk_common_1.AttributeReference.InvalidReference;
}
});
});
}
exports.processSegment = processSegment;
function tryParse(data) {
try {
return JSON.parse(data);
}
catch (_a) {
return undefined;
}
}
/**
* @internal
*/
function deserializeAll(data) {
var _a, _b;
// The reviver lacks the context of where a different key exists, being as it
// starts at the deepest level and works outward. As a result attributes are
// translated into references after the initial parsing. That way we can be sure
// they are the correct ones. For instance if we added 'attribute' as a new field to
// the schema for something that was NOT an attribute reference, then we wouldn't
// want to construct an attribute reference out of it.
const parsed = tryParse(data);
if (!parsed) {
return undefined;
}
Object.values(((_a = parsed === null || parsed === void 0 ? void 0 : parsed.data) === null || _a === void 0 ? void 0 : _a.flags) || []).forEach((flag) => {
processFlag(flag);
});
Object.values(((_b = parsed === null || parsed === void 0 ? void 0 : parsed.data) === null || _b === void 0 ? void 0 : _b.segments) || []).forEach((segment) => {
processSegment(segment);
});
return parsed;
}
exports.deserializeAll = deserializeAll;
/**
* This function is intended for usage inside LaunchDarkly SDKs.
* This function should NOT be used by customer applications.
* This function may be changed or removed without a major version.
*
* @param data String data from launchdarkly.
* @returns The parsed and processed data.
*/
function deserializePoll(data) {
const parsed = tryParse(data);
if (!parsed) {
return undefined;
}
Object.values((parsed === null || parsed === void 0 ? void 0 : parsed.flags) || []).forEach((flag) => {
processFlag(flag);
});
Object.values((parsed === null || parsed === void 0 ? void 0 : parsed.segments) || []).forEach((segment) => {
processSegment(segment);
});
return parsed;
}
exports.deserializePoll = deserializePoll;
/**
* @internal
*/
function deserializePatch(data) {
const parsed = tryParse(data);
if (!parsed) {
return undefined;
}
if (parsed.path.startsWith(VersionedDataKinds_1.default.Features.streamApiPath)) {
processFlag(parsed.data);
parsed.kind = VersionedDataKinds_1.default.Features;
}
else if (parsed.path.startsWith(VersionedDataKinds_1.default.Segments.streamApiPath)) {
processSegment(parsed.data);
parsed.kind = VersionedDataKinds_1.default.Segments;
}
return parsed;
}
exports.deserializePatch = deserializePatch;
/**
* @internal
*/
function deserializeDelete(data) {
const parsed = tryParse(data);
if (!parsed) {
return undefined;
}
if (parsed.path.startsWith(VersionedDataKinds_1.default.Features.streamApiPath)) {
parsed.kind = VersionedDataKinds_1.default.Features;
}
else if (parsed.path.startsWith(VersionedDataKinds_1.default.Segments.streamApiPath)) {
parsed.kind = VersionedDataKinds_1.default.Segments;
}
return parsed;
}
exports.deserializeDelete = deserializeDelete;
/**
* Serialize a single flag. Used for persistent data stores.
*
* @internal
*/
function serializeFlag(flag) {
return JSON.stringify(flag, replacer);
}
exports.serializeFlag = serializeFlag;
/**
* Deserialize a single flag. Used for persistent data stores.
*
* @internal
*/
function deserializeFlag(data) {
const parsed = tryParse(data);
if (!parsed) {
return undefined;
}
processFlag(parsed);
return parsed;
}
exports.deserializeFlag = deserializeFlag;
/**
* Serialize a single segment. Used for persistent data stores.
*
* @internal
*/
function serializeSegment(segment) {
return JSON.stringify(segment, replacer);
}
exports.serializeSegment = serializeSegment;
/**
* Deserialize a single segment. Used for persistent data stores.
*
* @internal
*/
function deserializeSegment(data) {
const parsed = tryParse(data);
if (!parsed) {
return undefined;
}
processSegment(parsed);
return parsed;
}
exports.deserializeSegment = deserializeSegment;
//# sourceMappingURL=serialization.js.map
;