diff --git a/index.d.ts b/index.d.ts index 206b058..425e9d4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -338,6 +338,11 @@ declare module "ldclient-node" { */ initialized: () => boolean; + /** + * @returns a Promise containing the initialization state of the client + */ + waitUntilReady: () => Promise; + /** * Retrieves a flag's value. * @@ -354,21 +359,24 @@ declare module "ldclient-node" { * * @param callback * The callback to receive the variation result. + * + * @returns a Promise containing the flag value */ - variation: (key: string, user: LDUser, defaultValue: LDFlagValue, callback?: (err: any, res: LDFlagValue) => void) => void; + variation: (key: string, user: LDUser, defaultValue: LDFlagValue, callback?: (err: any, res: LDFlagValue) => void) => Promise; - toggle: (key: string, user: LDUser, defaultValue: LDFlagValue, callback?: (err: any, res: LDFlagValue) => void) => void; + toggle: (key: string, user: LDUser, defaultValue: LDFlagValue, callback?: (err: any, res: LDFlagValue) => void) => Promise; /** - * Retrieves a flag's value. + * Retrieves the set of all flag values for a user. * * @param key * The key of the flag for which to retrieve the corresponding value. * @param user * @param callback * The node style callback to receive the variation result. + * @returns a Promise containing the set of all flag values for a user */ - all_flags: (user: LDUser, callback?: (err: any, res: LDFlagSet) => void) => void; + all_flags: (user: LDUser, callback?: (err: any, res: LDFlagSet) => void) => Promise; /** * @@ -430,7 +438,9 @@ declare module "ldclient-node" { * Internally, the LaunchDarkly SDK keeps an event queue for track and identify calls. * These are flushed periodically (see configuration option: flush_interval) * and when the queue size limit (see configuration option: capacity) is reached. + * + * @returns a Promise which resolves once flushing is finished */ - flush: (callback: (err: any, res: boolean) => void) => void; + flush: (callback?: (err: any, res: boolean) => void) => Promise; } } diff --git a/index.js b/index.js index a9da0b5..97d0369 100644 --- a/index.js +++ b/index.js @@ -13,9 +13,33 @@ var async = require('async'); var errors = require('./errors'); var package_json = require('./package.json'); -var noop = function(){}; +/** + * Wrap a promise to invoke an optional callback upon resolution or rejection. + * + * This function assumes the callback follows the Node.js callback type: (err, value) => void + * + * If a callback is provided: + * - if the promise is resolved, invoke the callback with (null, value) + * - if the promise is rejected, invoke the callback with (error, null) + * + * @param {Promise} promise + * @param {Function} callback + * @returns Promise + */ +function wrapPromiseCallback(promise, callback) { + if (callback) { + return promise.then( + function(value) { + setTimeout(function() { callback(null, value); }, 0); + }, + function(error) { + setTimeout(function() { callback(error, null); }, 0); + } + ); + } -global.setImmediate = global.setImmediate || process.nextTick.bind(process); + return promise; +} function createErrorReporter(emitter, logger) { return function(error) { @@ -31,6 +55,8 @@ function createErrorReporter(emitter, logger) { }; } +global.setImmediate = global.setImmediate || process.nextTick.bind(process); + var new_client = function(sdk_key, config) { var client = new EventEmitter(), init_complete = false, @@ -105,104 +131,104 @@ var new_client = function(sdk_key, config) { return init_complete; } - client.variation = function(key, user, default_val, fn) { - sanitize_user(user); - var cb = fn || noop; - var variationErr; + client.waitUntilReady = function() { + return new Promise(function(resolve) { + client.once('ready', resolve); + }); + }; - if (this.is_offline()) { - config.logger.info("[LaunchDarkly] variation called in offline mode. Returning default value."); - cb(null, default_val); - return; - } + client.variation = function(key, user, default_val, callback) { + return wrapPromiseCallback(new Promise(function(resolve, reject) { + sanitize_user(user); + var variationErr; - else if (!key) { - variationErr = new errors.LDClientError('No feature flag key specified. Returning default value.'); - maybeReportError(variationError); - send_flag_event(key, user, default_val, default_val); - cb(variationErr, default_val); - return; - } + if (this.is_offline()) { + config.logger.info("[LaunchDarkly] variation called in offline mode. Returning default value."); + return resolve(default_val); + } - else if (!user) { - variationErr = new errors.LDClientError('No user specified. Returning default value.'); - maybeReportError(variationErr); - send_flag_event(key, user, default_val, default_val); - cb(variationErr, default_val); - return; - } + else if (!key) { + variationErr = new errors.LDClientError('No feature flag key specified. Returning default value.'); + maybeReportError(variationError); + send_flag_event(key, user, default_val, default_val); + return resolve(default_val); + } - else if (user.key === "") { - config.logger.warn("[LaunchDarkly] User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly"); - } + else if (!user) { + variationErr = new errors.LDClientError('No user specified. Returning default value.'); + maybeReportError(variationErr); + send_flag_event(key, user, default_val, default_val); + return resolve(default_val); + } - if (!init_complete) { - variationErr = new errors.LDClientError("Variation called before LaunchDarkly client initialization completed (did you wait for the 'ready' event?)"); - maybeReportError(variationErr); - send_flag_event(key, user, default_val, default_val); - cb(variationErr, default_val); - return; - } + else if (user.key === "") { + config.logger.warn("[LaunchDarkly] User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly"); + } + + if (!init_complete) { + variationErr = new errors.LDClientError("Variation called before LaunchDarkly client initialization completed (did you wait for the 'ready' event?)"); + maybeReportError(variationErr); + send_flag_event(key, user, default_val, default_val); + return resolve(default_val); + } - config.feature_store.get(key, function(flag) { - evaluate.evaluate(flag, user, config.feature_store, function(err, result, events) { - var i; - var version = flag ? flag.version : null; + config.feature_store.get(key, function(flag) { + evaluate.evaluate(flag, user, config.feature_store, function(err, result, events) { + var i; + var version = flag ? flag.version : null; - if (err) { - maybeReportError(new errors.LDClientError('Encountered error evaluating feature flag:' + (err.message ? (': ' + err.message) : err))); - } + if (err) { + maybeReportError(new errors.LDClientError('Encountered error evaluating feature flag:' + (err.message ? (': ' + err.message) : err))); + } - // Send off any events associated with evaluating prerequisites. The events - // have already been constructed, so we just have to push them onto the queue. - if (events) { - for (i = 0; i < events.length; i++) { - enqueue(events[i]); + // Send off any events associated with evaluating prerequisites. The events + // have already been constructed, so we just have to push them onto the queue. + if (events) { + for (i = 0; i < events.length; i++) { + enqueue(events[i]); + } } - } - if (result === null) { - config.logger.debug("[LaunchDarkly] Result value is null in variation"); - send_flag_event(key, user, default_val, default_val, version); - cb(null, default_val); - return; - } else { - send_flag_event(key, user, result, default_val, version); - cb(null, result); - return; - } + if (result === null) { + config.logger.debug("[LaunchDarkly] Result value is null in variation"); + send_flag_event(key, user, default_val, default_val, version); + return resolve(default_val); + } else { + send_flag_event(key, user, result, default_val, version); + return resolve(result); + } + }); }); - }); + }.bind(this)), callback); } - client.toggle = function(key, user, default_val, fn) { + client.toggle = function(key, user, default_val, callback) { config.logger.warn("[LaunchDarkly] toggle is deprecated. Call 'variation' instead"); - client.variation(key, user, default_val, fn); + return client.variation(key, user, default_val, callback); } - client.all_flags = function(user, fn) { - sanitize_user(user); - var cb = fn || noop; - var results = {}; - - if (this.is_offline() || !user) { - config.logger.info("[LaunchDarkly] all_flags called in offline mode. Returning empty map."); + client.all_flags = function(user, callback) { + return wrapPromiseCallback(new Promise(function(resolve, reject) { + sanitize_user(user); + var results = {}; - cb(null, null); - return; - } + if (this.is_offline() || !user) { + config.logger.info("[LaunchDarkly] all_flags called in offline mode. Returning empty map."); + return resolve({}); + } - config.feature_store.all(function(flags) { - async.forEachOf(flags, function(flag, key, iteratee_cb) { - // At the moment, we don't send any events here - evaluate.evaluate(flag, user, config.feature_store, function(err, result, events) { - results[key] = result; - iteratee_cb(null); - }) - }, function(err) { - cb(err, results); + config.feature_store.all(function(flags) { + async.forEachOf(flags, function(flag, key, iteratee_cb) { + // At the moment, we don't send any events here + evaluate.evaluate(flag, user, config.feature_store, function(err, result, events) { + results[key] = result; + iteratee_cb(null); + }) + }, function(err) { + return err ? reject(err) : resolve(results); + }); }); - }); + }.bind(this)), callback); } client.secure_mode_hash = function(user) { @@ -245,36 +271,31 @@ var new_client = function(sdk_key, config) { enqueue(event); }; - client.flush = function(fn) { - var cb = fn || noop; - var worklist; - if (!queue.length) { - return process.nextTick(cb); - } - - worklist = queue.slice(0); - queue = []; - - config.logger.debug("Flushing %d events", worklist.length); + client.flush = function(callback) { + return wrapPromiseCallback(new Promise(function(resolve, reject) { + var worklist; + if (!queue.length) { + resolve(); + } - requestify.request(config.events_uri + '/bulk', { - method: "POST", - headers: { - 'Authorization': sdk_key, - 'User-Agent': config.user_agent, - 'Content-Type': 'application/json' - }, - body: worklist, - timeout: config.timeout * 1000, - agent: config.proxy_agent - }) - .then(function(response) { - cb(null, response); - return; - }, function(error) { - cb(error, null); - return; - }); + worklist = queue.slice(0); + queue = []; + + config.logger.debug("Flushing %d events", worklist.length); + + requestify.request(config.events_uri + '/bulk', { + method: "POST", + headers: { + 'Authorization': sdk_key, + 'User-Agent': config.user_agent, + 'Content-Type': 'application/json' + }, + body: worklist, + timeout: config.timeout * 1000, + agent: config.proxy_agent + }) + .then(resolve, reject); + }.bind(this)), callback); }; function enqueue(event) { diff --git a/tsconfig.json b/tsconfig.json index 10580fe..918b0e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { "module": "commonjs", - "strict": true + "strict": true, + "lib": [ + "es6" + ] } } \ No newline at end of file