Skip to content
This repository has been archived by the owner on May 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #8 from launchdarkly/ag/ch2734/add-promise-interface
Browse files Browse the repository at this point in the history
(RFC) [ch2734] Add promise interface
  • Loading branch information
apucacao authored Aug 8, 2017
2 parents fba5346 + 637aa06 commit 614f791
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 116 deletions.
20 changes: 15 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ declare module "ldclient-node" {
*/
initialized: () => boolean;

/**
* @returns a Promise containing the initialization state of the client
*/
waitUntilReady: () => Promise<void>;

/**
* Retrieves a flag's value.
*
Expand All @@ -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<LDFlagValue>;

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<LDFlagValue>;

/**
* 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<LDFlagSet>;

/**
*
Expand Down Expand Up @@ -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<void>;
}
}
241 changes: 131 additions & 110 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>} promise
* @param {Function} callback
* @returns Promise<any>
*/
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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
"strict": true
"strict": true,
"lib": [
"es6"
]
}
}

0 comments on commit 614f791

Please sign in to comment.