@launchdarkly/js-server-sdk-common
Version:
LaunchDarkly Server SDK for JavaScript - common code
167 lines • 9.28 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");
class StreamingProcessorFDv2 {
constructor(clientContext, _streamUriPath, _parameters, baseHeaders, _diagnosticsManager, _streamInitialReconnectDelay = 1,
// set when this source needs its own endpoint in a composite chain; leave undefined
// to fall back to the shared config.serviceEndpoints
serviceEndpointsOverride) {
this._streamUriPath = _streamUriPath;
this._parameters = _parameters;
this._diagnosticsManager = _diagnosticsManager;
this._streamInitialReconnectDelay = _streamInitialReconnectDelay;
const { basicConfiguration, platform } = clientContext;
const { logger, serviceEndpoints } = basicConfiguration;
const { requests } = platform;
this._headers = Object.assign({}, baseHeaders);
this._serviceEndpoints = serviceEndpointsOverride !== null && serviceEndpointsOverride !== void 0 ? serviceEndpointsOverride : serviceEndpoints;
this._logger = logger;
this._requests = requests;
}
_logConnectionAttempt() {
this._connectionAttemptStartTime = Date.now();
}
_logConnectionResult(success) {
if (this._connectionAttemptStartTime && this._diagnosticsManager) {
this._diagnosticsManager.recordStreamInit(this._connectionAttemptStartTime, !success, Date.now() - this._connectionAttemptStartTime);
}
this._connectionAttemptStartTime = undefined;
}
/**
* This is a wrapper around the passed errorHandler which adds additional
* diagnostics and logging logic.
*
* @param err The error to be logged and handled.
* @return boolean whether to retry the connection.
*
* @private
*/
_retryAndHandleError(err, statusCallback) {
var _a, _b, _c;
// this is a short term error and will be removed once FDv2 adoption is sufficient.
if (((_a = err.headers) === null || _a === void 0 ? void 0 : _a[`x-ld-fd-fallback`]) === `true`) {
const fallbackErr = new js_sdk_common_1.LDFlagDeliveryFallbackError(js_sdk_common_1.DataSourceErrorKind.ErrorResponse, `Response header indicates to fallback to FDv1`, err.status);
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Closed, fallbackErr);
return false;
}
if (!(0, js_sdk_common_1.shouldRetry)(err)) {
(_b = this._logger) === null || _b === void 0 ? void 0 : _b.error((0, js_sdk_common_1.httpErrorMessage)(err, 'streaming request'));
this._logConnectionResult(false);
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Closed, new js_sdk_common_1.LDStreamingError(js_sdk_common_1.DataSourceErrorKind.ErrorResponse, err.message, err.status, false));
return false;
}
(_c = this._logger) === null || _c === void 0 ? void 0 : _c.warn((0, js_sdk_common_1.httpErrorMessage)(err, 'streaming request', 'will retry'));
this._logConnectionResult(false);
this._logConnectionAttempt();
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Interrupted);
return true;
}
start(dataCallback, statusCallback, selectorGetter) {
var _a;
this._logConnectionAttempt();
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Initializing);
const selector = selectorGetter === null || selectorGetter === void 0 ? void 0 : selectorGetter();
const params = selector
? [...this._parameters, { key: 'basis', value: selector }] // if selector exists add basis parameter
: this._parameters; // otherwise use params as is
const uri = (0, js_sdk_common_1.getStreamingUri)(this._serviceEndpoints, this._streamUriPath, params);
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug(`Streaming processor opening event source to uri: ${uri}`);
// Set when the most recent successful connection carried `x-ld-fd-fallback: true`. We
// finish applying the next payload before emitting the fallback signal so evaluations
// can serve the server-provided data while the FDv1 synchronizer takes over.
let fallbackRequested = false;
const eventSource = this._requests.createEventSource(uri, {
headers: this._headers,
errorFilter: (error) => this._retryAndHandleError(error, statusCallback),
initialRetryDelayMillis: 1000 * this._streamInitialReconnectDelay,
readTimeoutMillis: 5 * 60 * 1000,
retryResetIntervalMillis: 60 * 1000,
});
this._eventSource = eventSource;
const payloadReader = new js_sdk_common_1.internal.PayloadStreamReader(eventSource, {
flag: (flag) => {
(0, serialization_1.processFlag)(flag);
return flag;
},
segment: (segment) => {
(0, serialization_1.processSegment)(segment);
return segment;
},
}, (errorKind, message) => {
var _a;
// If a parse 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 LDStreamingError as an ordinary failure and fall through to the next
// FDv2 source.
if (fallbackRequested) {
const fallbackErr = new js_sdk_common_1.LDFlagDeliveryFallbackError(js_sdk_common_1.DataSourceErrorKind.ErrorResponse, `Response header indicates to fallback to FDv1`);
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn(fallbackErr.message);
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Closed, fallbackErr);
}
else {
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Interrupted, new js_sdk_common_1.LDStreamingError(errorKind, message));
}
// parsing error was encountered, defensively close the data source
this.stop();
}, this._logger);
payloadReader.addPayloadListener((payload) => {
var _a;
this._logConnectionResult(true);
// The server may signal FDv1 fallback alongside a valid streaming payload via the
// response headers on the initial connection. Attach a fallbackToFDv1 marker to the
// data callback so the directive is delivered atomically with the payload --
// CompositeDataSource will swap its synchronizer list to FDv1 before resolving the
// switchToSync transition. A separate status callback after the data callback would
// be silently dropped because the basis-during-init auto-transition disables the
// composite's callback handler.
const data = {
initMetadata: this._initMetadata,
payload,
};
if (fallbackRequested) {
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);
if (fallbackRequested) {
// Stop consuming the FDv2 stream now that the directive has been delivered.
this.stop();
}
});
eventSource.onclose = () => {
var _a;
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.info('Closed LaunchDarkly stream connection');
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Closed);
};
eventSource.onerror = () => {
// The work is done by `errorFilter`.
};
eventSource.onopen = (e) => {
var _a, _b;
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.info('Opened LaunchDarkly stream connection');
this._initMetadata = js_sdk_common_1.internal.initMetadataFromHeaders(e.headers);
// The fallback signal is captured here from the connection-open response headers and
// is honored by the payload listener above once the next payload has been applied.
if (((_b = e.headers) === null || _b === void 0 ? void 0 : _b[`x-ld-fd-fallback`]) === `true`) {
fallbackRequested = true;
}
statusCallback(js_sdk_common_1.subsystem.DataSourceState.Valid);
};
eventSource.onretrying = (e) => {
var _a;
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.info(`Will retry stream connection in ${e.delayMillis} milliseconds`);
};
}
stop() {
var _a;
(_a = this._eventSource) === null || _a === void 0 ? void 0 : _a.close();
this._eventSource = undefined;
}
close() {
this.stop();
}
}
exports.default = StreamingProcessorFDv2;
//# sourceMappingURL=StreamingProcessorFDv2.js.map