@launchdarkly/js-server-sdk-common
Version:
LaunchDarkly Server SDK for JavaScript - common code
197 lines • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const js_sdk_common_1 = require("@launchdarkly/js-sdk-common");
const serialization_1 = require("../store/serialization");
function selectorAsQueryParams(selector) {
if (!selector) {
return [];
}
return [
{
key: 'basis',
value: selector,
},
];
}
// helper function to transform FDv1 response data into events the PayloadProcessor can parse
function processFDv1FlagsAndSegments(payloadProcessor, data) {
const adaptor = js_sdk_common_1.internal.FDv1PayloadAdaptor(payloadProcessor);
adaptor.useSelector('FDv1Fallback').processFullTransfer(data);
}
/**
* @internal
*/
class PollingProcessorFDv2 {
/**
* @param _requestor to fetch flags
* @param _pollInterval in seconds controlling how frequently polling request is made
* @param _logger for logging
* @param _processResponseAsFDv1 defaults to false, but if set to true, this data source will process
* the response body as FDv1 and convert it into a FDv2 payload.
*/
constructor(_requestor, _pollInterval = 30, _logger, _processResponseAsFDv1 = false) {
this._requestor = _requestor;
this._pollInterval = _pollInterval;
this._logger = _logger;
this._processResponseAsFDv1 = _processResponseAsFDv1;
this._stopped = false;
}
_poll(dataCallback, statusCallback, selectorGetter) {
var _a;
if (this._stopped) {
return;
}
const startTime = Date.now();
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Polling LaunchDarkly for feature flag updates');
this._requestor.requestAllData((err, body, headers, fallbackToFDv1) => {
var _a, _b, _c, _d, _e, _f;
if (this._stopped) {
return;
}
const elapsed = Date.now() - startTime;
const sleepFor = Math.max(this._pollInterval * 1000 - elapsed, 0);
// Helper to emit the terminal FDv1 fallback signal. Callers must `return` after
// invoking this; no further poll is scheduled.
const emitFallback = () => {
var _a;
const fallbackErr = err instanceof js_sdk_common_1.LDFlagDeliveryFallbackError
? err
: new js_sdk_common_1.LDFlagDeliveryFallbackError(js_sdk_common_1.DataSourceErrorKind.ErrorResponse, err
? (0, js_sdk_common_1.httpErrorMessage)(err, 'polling request', 'falling back to FDv1')
: `Response header indicates to fallback to FDv1`, err === null || err === void 0 ? void 0 : err.status);
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn(fallbackErr.message);
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Closed, fallbackErr);
};
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Elapsed: %d ms, sleeping for %d ms', elapsed, sleepFor);
if (err) {
// The fallback directive can ride along on either an error or a successful response.
// Honor it before any other error handling so we always switch to FDv1 when asked.
if (fallbackToFDv1 || err instanceof js_sdk_common_1.LDFlagDeliveryFallbackError) {
emitFallback();
return;
}
const { status } = err;
if (status && !(0, js_sdk_common_1.isHttpRecoverable)(status)) {
const message = (0, js_sdk_common_1.httpErrorMessage)(err, 'polling request');
(_b = this._logger) === null || _b === void 0 ? void 0 : _b.error(message);
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Closed, new js_sdk_common_1.LDPollingError(js_sdk_common_1.DataSourceErrorKind.ErrorResponse, message, status, false));
// It is not recoverable, return and do not trigger another poll.
return;
}
const message = (0, js_sdk_common_1.httpErrorMessage)(err, 'polling request', 'will retry');
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Interrupted, new js_sdk_common_1.LDPollingError(js_sdk_common_1.DataSourceErrorKind.ErrorResponse, message, status));
(_c = this._logger) === null || _c === void 0 ? void 0 : _c.warn(message);
// schedule poll
this._timeoutHandle = setTimeout(() => {
this._poll(dataCallback, statusCallback, selectorGetter);
}, sleepFor);
return;
}
const initMetadata = js_sdk_common_1.internal.initMetadataFromHeaders(headers);
if (body) {
try {
const payloadProcessor = new js_sdk_common_1.internal.PayloadProcessor({
flag: (flag) => {
(0, serialization_1.processFlag)(flag);
return flag;
},
segment: (segment) => {
(0, serialization_1.processSegment)(segment);
return segment;
},
}, (errorKind, message) => {
// If a per-event error fires while the fallback directive is in flight, route
// it through the LDFlagDeliveryFallbackError path so CompositeDataSource still
// engages FDv1. Otherwise the directive would be lost: the composite's status
// handler would treat the LDPollingError as an ordinary failure and fall
// through to the next FDv2 source.
if (fallbackToFDv1) {
emitFallback();
return;
}
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Interrupted, new js_sdk_common_1.LDPollingError(errorKind, message));
}, this._logger);
// When the fallback directive rides along on a 200 response with a valid payload,
// the directive must be applied atomically with the data callback. CompositeData-
// Source disables its callback handler as soon as basis-during-init arrives or an
// error status is reported, so a separate status callback emitted afterwards
// would be silently dropped. Instead, attach a fallbackToFDv1 marker to the data
// object: CompositeDataSource will swap its synchronizer list to FDv1 before
// resolving the switchToSync transition.
payloadProcessor.addPayloadListener((payload) => {
var _a;
const data = {
initMetadata,
payload,
};
if (fallbackToFDv1) {
data.fallbackToFDv1 = true;
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn(`Response header indicates to fallback to FDv1`);
}
dataCallback(payload.type === 'full', data);
});
(_d = this._logger) === null || _d === void 0 ? void 0 : _d.debug(`Got body: ${body}`);
if (!this._processResponseAsFDv1) {
// FDv2 case
const parsed = JSON.parse(body);
payloadProcessor.processEvents(parsed.events);
}
else {
// FDv1 case
const parsed = JSON.parse(body);
processFDv1FlagsAndSegments(payloadProcessor, parsed);
}
// The fallback directive (if any) was attached to the data callback above so
// CompositeDataSource has already taken over the transition. Skip the Valid
// status report and the next-poll scheduling in that case.
if (fallbackToFDv1) {
return;
}
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Valid);
}
catch (_g) {
// We could not parse this JSON. Report the problem and fallthrough to
// start another poll.
(_e = this._logger) === null || _e === void 0 ? void 0 : _e.error('Response contained invalid data');
(_f = this._logger) === null || _f === void 0 ? void 0 : _f.debug(`${err} - Body follows: ${body}`);
// The fallback directive must be honored even when the body could not be parsed.
// Emit it BEFORE the generic Interrupted/LDPollingError status callback below --
// CompositeDataSource disables its callback handler on the first error status,
// so the LDFlagDeliveryFallbackError must be emitted first or the directive is
// silently dropped and the SDK retries FDv2 sources.
if (fallbackToFDv1) {
emitFallback();
return;
}
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Interrupted, new js_sdk_common_1.LDPollingError(js_sdk_common_1.DataSourceErrorKind.InvalidData, 'Malformed data in polling response'));
}
}
else if (fallbackToFDv1) {
// 200/304 with no body but a fallback directive: emit the directive directly.
emitFallback();
return;
}
// schedule poll
this._timeoutHandle = setTimeout(() => {
this._poll(dataCallback, statusCallback, selectorGetter);
}, sleepFor);
}, selectorAsQueryParams(selectorGetter === null || selectorGetter === void 0 ? void 0 : selectorGetter()));
}
start(dataCallback, statusCallback, selectorGetter) {
this._statusCallback = statusCallback; // hold reference for usage in stop()
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Initializing);
this._poll(dataCallback, statusCallback, selectorGetter);
}
stop() {
var _a;
if (this._timeoutHandle) {
clearTimeout(this._timeoutHandle);
this._timeoutHandle = undefined;
}
(_a = this._statusCallback) === null || _a === void 0 ? void 0 : _a.call(this, js_sdk_common_1.subsystem.DataSourceState.Closed);
this._stopped = true;
this._statusCallback = undefined;
}
}
exports.default = PollingProcessorFDv2;
//# sourceMappingURL=PollingProcessorFDv2.js.map