sinch

0.4.0 • Public • Published

Sinch


Build Status


Sinch sends your asynchronous code into a patent-pending parallel dimension where time has no meaning and everything acts as though it's synchronous.

Don't worry, your code will still run asynchronously, just the way your wrote it. Sinch isn't some sort of fly-by-night snake oil that promises the moon and delivers an empty bottle of promises.

Even if Sinch were snake oil, it would be the classy kind of snake oil that's actually fine whiskey and only pretends to be medicinal to get around Prohibition-era liquor laws. Except in this case, think of "medicinal" as "synchronous" and "Prohibition-era liquor laws" as "callbacks", and "fine whiskey" as "rock-solid code with a full test barrage and one dead-simple API method."

Why would you ever go back to drinking bathtub hooch?

So nudge nudge, wink wink, why not enjoy our "synchronous" API?

Want to return objects from asynchronous calls and call methods of those objects immediately? Go ahead.

Want to follow the convention of having your callbacks as the last argument, but hate having to shift variables around for optional parameters? That doesn't exist at all in our parallel dimension.

Want to be able to use callbacks or return statements where they makes sense, but have your classes and APIs always work consistently? Done.

Want to protect your interface's object internal data from pesky developers? That's automatic.

Want to chain procedurally asynchronous interfaces which return other asynchronous interfaces without executing a single thing until you need the data? You can do that now.

Want to pass the results of an asynchronous method to any other function as if it were a normal everyday argument? You can do that. No, really.

Want to make make an asynchronous GET request to a remote server, and pass it to a method that looks in the content to see if there are any bees specified, then spawns and returns a new BeeCollection(), full of buzzy asynchronous bees who only exist in the future, which you can then manipulate at will, and then pass that BeeCollection into a Template class that outputs a HTML list of all your favorite bees, along with their temperaments, all without writing a single callback? It's a little bit of an odd request, but you can totally do that.

You might be asking yourself, How is this even possible?, and hoping that I'd stop talking about parallel dimensions and 1920's American history and just tell you how the damn thing works.

Sinch uses a callback at the very end of your execution chain to see what data came from all of it, which kicks off a huge queue of everything leading up to that value. When you pass the "results" of Sinch API operation to another Sinch function, the data will be desynchronized using a hidden callback before being passed into the other Sinch function.


Boring Technical Description

Sinch is a lightweight, powerful JavaScript module which allows developers to create APIs which act in a consistent manner regardless of whether they are synchronous (returning their values immediately) or asynchronous (forcing the user to utilize a callback).

Any arguments passed to a Sinch-wrapped function can be manipulated as normal, synchronous arguments. Using this call-forward pattern, end users are able to pass methods from one Sinch API to another without having to understand that the operations are asynchronous at all.

When objects are passed to Sinch, they will be returned as prototype-based JavaScript classes. Even if an instantiated object takes some time to initialize, it may be utilized immediately in a procedural fashion, with all data accessible at the end of the chain using callbacks, or passable to other Sinch-based API endpoints. Once the object has been fully initialized, all queued commands will be run immediately, properly bound to the newly-created scope.

Sinch also provides an Extends: keyword to objects which allows for the recursive extension of one Sinch API by another.

Examples

Single Functions

Let's start with two simple asynchronous function which call other functions with their values.

function echo(message, callback) {
	callback("I'm echoing, "+message+".");
}

function log(message) {
	console.log(message);
}

If we were to call echo("hello world", console.log), we would get the response of, "I'm echoing, hello world." This is how traditional modules present their asynchronous methods.

The problem with that is that the end user must create an intermediary anonymous function each time it's necessary to pass a value from one endpoint to another. This code would end up logging to the console, "I'm echoing, hello world."

echo('hello world', function(message) {
	log(message);
});

This is a pattern that JavaScript developers are very used to! However, if we were to wrap both log and echo into Sinch, we would--without sacrificing our asynchronous nature--be able to present a much clear API to developers using our code, which is indistinguishable from procedural code.

This code would have the exact same effect as the previous code. In fact, the previous code can still be used when wrapping methods in Sinch.

var echo = sinch(function(message, callback) {
	this.callback("I'm echoing, "+message+".");
});

var log = sinch(function(message) {
	console.log(message);
});

log(echo(message));

Let's try something a little less abstract, and assume we're using a Sinch- based API which handles file transfers. We want to chain a lot of asynchronous file operations together in order to piece together a file, and use dynamic arguments, with the last argument being considered a callback if it's a function. This also requires that we keep track of how many asynchronous operations are pending and nest callbacks.

The code for something like this can get ugly fairly quickly!

function cat() {
	var output, i, files = [], callback;
	function next() {
		if (!callback) return;
		for (var f in files) if (files[f]) output += files[f];
		callback(output);
	}
	for (i in arguments) {
		// Dynamic arguments; check the last argument for a callback.
		if (i==arguments.length-1 && typeof arguments[i]=="function") {
			callback = arguments[i];
		}
		pending++;
		async.file_exists(arguments[i], function(success) {
			pending--;
			if (pending==0) next();
			if (!success) return;
			pending++;
			async.file_read(arguments[i], function(data) {
				pending--;
				files[i] = data;
				if (pending==0) next();
			});
		});
	}
}

cat('readme.txt', 'file.txt', 'other.txt', console.log);

Assuming we didn't want to reinvent the file API, we could refactor the same code using Sinch to be much easier to understand, more pleasant to use, and far less error prone.

function cat() {
	var output = "";
	for (var i in arguments) output += arguments[i];
	return output;
}); cat = sinch(cat);

function read(file, callback) {
	async.file_exists(file, function(exists) {
		if (!exists) callback('');
		else async.file_read(file, callback);
	});
}; read = sinch(read);

cat(read('readme.txt'), read('file.txt'), read('other.txt'), console.log);

We could even make the example do even more advanced things without sacrificing readability or adding much code.

function get(url) { $.ajax(url, callback); }
get = sinch(get);

cat(read('readme.txt'), get('data.json'), read('other.txt'), console.log);

And, since Sinch doesn't modify the original values of the objects passed into it, and follows the same standard callback pattern of other JavaScript code, we could simply our code even further.

get = sinch($.ajax);

Dynamic Arguments

Sinch provides this.callback to all wrapped functions and methods to allow for explicit callback support, which means when we're traversing a dynamic arguments list, we don't have to manually check the last argument and infer whether or not it's meant to be a callback.

Otherwise, you'd have to do something like this in every function which takes dynamic arguments and a callback:

for (var i in arguments) {
	if (i==arguments.length-1 && typeof arguments[i]=="function") {
		callback = arguments[i];
	} else {
		//Do your stuff here.
	}
}

Optional Parameters with Callback Support

It also means we can support optional parameters, and error check on them, without having to worry about an argument possibly being a callback instead of what we're expecting.

For example, let's say we want to create a function called outputMessage with an optional second parameter, and by convention the callback parameter is always last.

Without Sinch, we would have to do this:

function outputMessage(msg,name,cb) {
	if (typeof name=="function") {
		cb = name;
		name = "Someone";
	}
	return name+" said: ", msg;
}

However, using Sinch, we can make the code much more readable, and focus on application logic rather than parsing our arguments.

var outputMessage = sinch(function(msg,name) {
	name = name || "Someone";
	return name+" said: "+ msg;
});

Or, asynchronously:

var outputMessage = sinch(function(msg,name) {
	name = name || "Someone";
	this.callback(name+" said: "+ msg);
});

Objects

If we pass an object into Sinch, we'll get back a constructor function with methods attached to its prototype.

var Database = sinch({
	
	dblib: require('dblib'),
	
	execute: function(op) {
		return this.dblib.execute(op);
	},
	
	find: function(filter, callback) {
		this.execute('find '+filter, callback);
	}
	
});

var db = new Database();
db.find('id=1', console.log);

We have two magic property names within this object, init and Extends.

Asynchronous Initialization

Providing an init method tells Sinch that this object will take some time to initialize, and that its interface should be presented through a dummy mechanism and then queued to be later executed when the object is ready.

For example:

var Database = sinch({

	// [...]
	init: function(server, db) {
		var callback = this.callback;
		this.dblib.connect(server, db, function() {
			callback();
		});
	}
	
});

Note that we can use the exact same code from the previous example to utilize the Database API. Each method will be executed when all required objects have initialized.

var db = new Database();
db.find('id=1', console.log);

Extending

We could also use Extends to extend one API from another.

var LiteDB = sinch({

	Extends: Database,
	
	dblib: require('dblite')
	
});

Interfaces

With the simple use of a [type, function] syntax within object definition, you can let Sinch know it should return a dummy interface of type, and run it once everything has been initialized.

var Cat = sinch({

	init: function(name, callback) {
		console.log("Waking up a cat named", name);
		setTimeout(callback, 1000);
	},
	
	catchMouse: [Mouse, function() { 
		return new Mouse();
	}]
	
});

This allows the user of the interface to work with the returned object as if it were immediately available.

new Cat('Meow-Meow').catchMouse().squeak(console.log);

Notice that in catchMouse, we can type simply, return new Mouse(), despite the fact that Mouse might or might not finish loading immediately, and Cat definitely won't.

All of the above methods of writing code are designed to work together. Try them out and see which patterns seem cleanest to you.

Roadmap

  • Error handling with traditional (e,data) callback structure.
  • Configurable timeouts which throw errors

Readme

Keywords

none

Package Sidebar

Install

npm i sinch

Weekly Downloads

3

Version

0.4.0

License

none

Last publish

Collaborators

  • yuffster